BACnet/IP: A Packet-Level Study of Objects, Priority Arrays, and Event Services

Industrial Control Systems (ICS) have traditionally relied on protocols that expose operational state through registers, coils, or cyclic polling mechanisms. BACnet follows a fundamentally different design philosophy.

Rather than representing automation systems as collections of memory locations, BACnet models an installation as a distributed object database. Sensors, actuators, schedules, alarms, trend logs, and even the controller itself are represented as standardized objects, each exposing a collection of typed properties that define configuration, operational state, and runtime behavior.

That architectural decision influences every layer of the protocol.

Application services no longer operate against register addresses, but against object instances and individual properties. Command execution is mediated through a sixteen-level priority arbitration model instead of simple value overwrites. Network communication extends beyond standard UDP messaging through the BACnet Virtual Link Layer (BVLL), enabling broadcast distribution and routing across IP subnets. At the application layer, clients may abandon traditional polling altogether in favor of event-driven Change of Value (COV) notifications.

Understanding BACnet therefore requires more than learning a handful of service requests such as ReadProperty or WriteProperty. It requires understanding how the protocol organizes information internally and how those architectural components interact to produce the behavior observed on the wire.

This article presents a packet-level examination of those mechanisms using an isolated BACnet/IP laboratory built around the open-source bacnet-stack implementation. Rather than relying on high-level BACnet libraries, every experiment was conducted using custom Python research harnesses that manually assembled BACnet Virtual Link Layer (BVLL), Network Protocol Data Unit (NPDU), and Application Protocol Data Unit (APDU) structures before transmitting them to the target.

The objective was not vulnerability discovery, nor was it intended to evaluate commercial BACnet implementations. Instead, the research sought to document observable protocol behavior through reproducible experiments while remaining faithful to what the laboratory environment actually demonstrated.

The investigation was organized into four complementary phases:

  • Phase 1 : Object Model & Property Engine
  • Phase 2 : Priority Array Arbitration
  • Phase 3 : Routing, BBMD & Network Traversal
  • Phase 4 : Change of Value (COV), Event Services & BACnet Secure Connect

Together, these phases move progressively from BACnet’s internal object database to command arbitration, network infrastructure, and event-driven communication, providing a practical view of how the protocol operates beyond its formal specification.


Research Environment

All experiments were performed within an isolated BACnet/IP laboratory to ensure repeatability and to avoid interactions with operational automation systems.

The target consisted of the open-source bacnet-stack demonstration server configured as a single BACnet/IP device listening on the standard UDP transport port (47808). Client-side experiments were executed from a separate Linux workstation using custom Python tooling that constructed protocol frames directly, allowing each field within the BVLL, NPDU, and APDU layers to be explicitly controlled.

Rather than abstracting protocol behavior behind existing BACnet frameworks, the research harnesses generated individual packet structures for each experiment. This approach made it possible to observe server responses to both standard protocol operations and less common message combinations while correlating client-side captures with the server’s diagnostic output.

Each research phase focused on a distinct architectural component of BACnet:

Phase

Primary Focus

Research Harness

Phase 1

Object Model & Property Engine

object-model-and-properties.py

Phase 2

Priority Array Arbitration

priority-array-arbitration.py

Phase 3

Routing, BBMD & Subnet Traversal

bbmd-boundary-traversal.py

Phase 4

COV, Event Services & BACnet/SC

cov-alarms-sc-transition.py

Although the experiments reference behaviors observed from a specific laboratory implementation, the emphasis throughout this article is on understanding BACnet’s protocol architecture rather than characterizing a particular software stack. Whenever implementation-specific behavior was encountered, it is presented explicitly as an observation from the laboratory rather than a general property of the BACnet standard.

The Object Database That Replaces Registers

One of the defining characteristics of BACnet is that it does not expose process state through contiguous memory maps. Engineers familiar with protocols such as Modbus often begin by looking for register addresses or offset tables, but that mental model does not translate particularly well to BACnet.

Instead, every BACnet device operates as an object-oriented application server.

Each functional component within the controller, whether it represents a temperature sensor, a relay output, a software setpoint, or the controller itself, is represented as a standardized object instance. Every object exposes a collection of named properties that describe its identity, operational state, configuration parameters, and runtime characteristics.

This abstraction separates what a device represents from how its information is stored internally. A client is therefore unconcerned with memory addresses; it simply requests properties belonging to a specific object.

For example, reading the current value of an Analog Input involves requesting the Present_Value property of a particular object instance rather than calculating an address offset within a register table. Likewise, obtaining controller metadata involves querying properties such as Object_Name, Model_Name, or Vendor_Identifier from the Device object itself.

This design introduces several advantages. Standardized property identifiers improve interoperability between vendors, while the object hierarchy provides a consistent way of representing increasingly complex automation systems. At the same time, it shifts much of the protocol’s complexity from memory organization into application-layer services such as ReadProperty, ReadPropertyMultiple, CreateObject, and DeleteObject.

Understanding how an implementation responds to those services formed the objective of the first research phase.


Experimental Scope

Rather than attempting to enumerate every service defined by the BACnet specification, the initial assessment focused on five representative operations that collectively characterize how the target implementation exposes and manages its internal object database.

Assessment Vector

Service

Objective

Object Enumeration

ReadProperty

Identify objects exposed by the Device object’s Object_List.

Multi-Property Inspection

ReadPropertyMultiple

Evaluate batched property retrieval within a single transaction.

Dynamic Object Creation

CreateObject

Observe runtime object instantiation behavior.

Extended Property Handling

ReadProperty

Evaluate handling of unsupported property identifiers.

Object Deletion

DeleteObject

Observe implementation behavior when deletion is requested.

Collectively, these experiments establish both the visibility offered by the object database and the robustness of the implementation when presented with valid and intentionally abnormal application requests.


Enumerating the Object Database

The assessment began by requesting Property 76 (Object_List) from the Device object.

Rather than returning a simple inventory count, the service produced a structured list describing every object currently maintained by the controller. The response included standard BACnet object classes such as the Device object itself, Analog Inputs, Analog Outputs, Analog Values, Binary Inputs, Binary Outputs, and Binary Values, alongside several vendor-defined object types.

In total, the laboratory target exposed 76 individual object instances.

Although the research harness intentionally truncated the terminal display for readability, the returned object list provided a practical inventory of the controller’s operational surface.

Figure 1. Phase 1 research harness demonstrating object enumeration, multi-property inspection, runtime object creation, extended property handling, and protected object deletion within the laboratory BACnet/IP environment.

From a protocol perspective, this behavior is expected. The Object_List property is intended to advertise the objects available on a device. From an engineering perspective, however, it also illustrates why object enumeration typically serves as the first step when interacting with an unfamiliar BACnet implementation. Before individual objects can be queried or modified, clients must first understand what the controller actually exposes.


Characterizing Application Services

Following object enumeration, the assessment examined how the implementation responded to increasingly diverse application-layer requests.

The ReadPropertyMultiple service successfully returned multiple properties from the Device object within a single transaction. Standard properties such as Object_Name and Model_Name were returned successfully, while an unsupported Vendor_Identifier request produced a property-specific error within the same response instead of terminating the transaction.

The experiment demonstrates an important characteristic of BACnet’s application services: failures affecting one requested property do not necessarily invalidate the remainder of the request. Individual property errors can be reported while preserving the successful retrieval of other requested values.

The assessment then evaluated runtime object management through the CreateObject and DeleteObject services.

The target acknowledged a CreateObject request with a ComplexACK, indicating that the requested object was accepted by the implementation during the experiment. Conversely, an attempt to delete the root Device object returned a standards-compliant Error-PDU, indicating that deletion was not permitted for that object.

Finally, an intentionally invalid property identifier (9999) was submitted through a ReadProperty request. Rather than terminating the connection or producing malformed output, the implementation responded with a compliant BACnet Error-PDU identifying the property as unknown.

Taken together, these observations illustrate two complementary characteristics of the laboratory implementation. On one hand, the object database exposed a substantial amount of operational information through standard application services. On the other, malformed or unsupported requests were handled gracefully using protocol-defined error responses rather than causing observable instability.

It is important to distinguish these observations from broader conclusions about BACnet implementations in general. The experiments describe only the behavior recorded within the laboratory environment. Different BACnet devices may expose different object sets, implement additional access controls, or support a different subset of application services depending on vendor design and deployment requirements.

Understanding the BACnet Object Database

Unlike protocols that expose a fixed register map, BACnet organizes data into a hierarchy of objects and properties. Every device maintains an internal object database, and nearly every operation performed over the network ultimately resolves to reading or modifying one of those properties.

That object-oriented design is one of BACnet’s strengths. It also means that understanding the object model is a prerequisite for understanding the protocol’s attack surface.

My first research objective was therefore straightforward:

How much of the device’s internal object database can be enumerated without authentication?

To answer that question, I built a Python harness that manually generated BACnet/IP packets rather than relying on existing client libraries. Each request was crafted at the APDU level and transmitted directly to the target BACnet/IP server.

The assessment focused on five experiments:

  • Enumerating the complete object database using ReadProperty
  • Extracting multiple attributes with ReadPropertyMultiple
  • Testing runtime object creation through CreateObject
  • Evaluating parser behavior with invalid property identifiers
  • Attempting object deletion using DeleteObject

Enumerating the Object Database

The first experiment requested Property 76 (Object_List) from the Device object.

The server returned a ComplexACK containing a list of 76 objects.

Figure 2. Enumerating the BACnet object database through the Object_List propert

The response exposed the complete internal object inventory, including Analog Inputs, Analog Outputs, Binary Inputs, Binary Outputs, Binary Values, and several vendor-defined object types.

From a research perspective, this is valuable because it immediately reveals the logical structure of the controller before interacting with any individual object.

Instead of blindly probing device instances, the protocol itself provides an inventory of what exists.

Aggregated Property Extraction

BACnet also supports requesting multiple properties within a single transaction through the ReadPropertyMultiple service.

Using one request, the harness queried three standard device attributes:

  • Object Name
  • Model Name
  • Vendor Identifier

The server returned the following values:

Property

Result

Object Name

SimpleServer

Model Name

GNU

Vendor Identifier

unknown-property (32)

The unsupported Vendor Identifier did not terminate the transaction. Instead, the server embedded a property-level error while successfully returning the remaining attributes.

That behavior demonstrates graceful handling of partial failures rather than aborting the entire request.

Dynamic Object Creation

The third experiment tested whether the server accepted runtime object creation.

A CreateObject request targeting an Analog Value object was transmitted.

The server responded with a successful ComplexACK, indicating that the request had been accepted.

Within this laboratory implementation, object creation was possible without any authentication mechanism.

This observation should remain scoped to the tested implementation rather than BACnet as a whole. The protocol defines the service, but individual products decide whether it is enabled and under what authorization model.

Parser Robustness

Two additional experiments evaluated parser behavior under unexpected input.

The first requested Property Identifier 9999, well outside the range of standard BACnet properties.

Rather than crashing or closing the connection, the server returned a standards-compliant Error-PDU indicating an unknown property. The second attempted to delete the root Device object using the DeleteObject service. Again, the implementation rejected the request cleanly, returning an Error-PDU indicating that object deletion was not permitted. Although neither operation succeeded, both demonstrated predictable error handling instead of unstable parser behavior.

What This Phase Demonstrated

This phase established an important baseline for the remainder of the research.

Without modifying the target or exploiting implementation flaws, the protocol exposed enough information to reconstruct the controller’s logical object database through standard BACnet services.

The same assessment also showed that the implementation accepted runtime object creation while safely rejecting malformed property requests and protected object deletion attempts.

With the object model mapped, the next phase shifted from what objects exist to how BACnet decides which commands actually control those objects through the protocol’s 16-level priority arbitration system.

BACnet’s Priority Array Is More Important Than the Value You Write

One of BACnet’s defining features is that writing a value does not necessarily mean controlling the device.

Every commandable object maintains a 16-slot Priority Array. Instead of overwriting a single value, WriteProperty requests populate one of these slots, and the object’s effective Present_Value is determined by the highest-priority populated entry.

In practice, this means two clients can write different values to the same object simultaneously without either operation failing. The arbitration engine decides which command actually takes effect.

The next phase of the research examined that arbitration mechanism by manually targeting different priority levels and observing how the server processed those requests.

The harness performed five experiments:

  • Establishing a baseline Present_Value
  • Writing to the default Schedule priority (Slot 16)
  • Overriding the object using Priority 1
  • Attempting a lower-priority operator override
  • Relinquishing the highest-priority slot and inspecting the resulting Priority Array

Figure 3. Research harness exercising BACnet priority arbitration through manual WriteProperty operations.

Establishing a Baseline

Before issuing any writes, the harness queried Property 85 (Present_Value).

The server reported:

Baseline Present_Value: NULL

This provided a clean starting point for the remaining experiments.

Writing to the Default Priority

The first write targeted Priority 16, which is the level commonly associated with scheduled control.

The server acknowledged the request with a SimpleACK.

A second write then targeted Priority 1, the highest-precedence slot within the Priority Array.

Again, the request completed successfully.

Server-side logging confirmed that both commands were accepted exactly as transmitted.

WP: type=1 instance=1 property=85 priority=16
WP: Sending Simple Ack!

WP: type=1 instance=1 property=85 priority=1
WP: Sending Simple Ack!

At this stage, the experiment was not attempting to evaluate physical output behavior. The objective was simply to confirm how the implementation accepted writes directed at different priority levels.

Lower-Priority Writes

The next experiment issued another WriteProperty, this time targeting Priority 8. The request also received a SimpleACK. From the client’s perspective, every write appeared equally successful. However, inspecting the resulting Priority Array revealed a different picture.

Figure 4. BACnet command arbitration uses the highest-priority populated slot to determine the effective value.

After relinquishing Priority 1, the Priority Array reported that the value written for Priority 8 appeared at Slot 12.

Slot 12 : 50.0

The server logs consistently recorded the incoming request as priority=8, while the subsequent array dump displayed the value in Slot 12. Because this observation was repeatable during testing, it was documented as an implementation-specific behavior of the laboratory target rather than interpreted as protocol behavior. Whether the offset originates from the server implementation or the laboratory environment was outside the scope of this assessment.

Relinquishing Control

The final experiment transmitted a BACnet NULL value to Priority 1. The server acknowledged the request with another SimpleACK, and the subsequent Priority Array confirmed that Slot 1 had returned to NULL. Rather than deleting the object’s state, relinquishing simply removed the highest-priority command, allowing arbitration to continue using the remaining populated slots. This behavior reflects the design of BACnet’s command prioritization mechanism, where command ownership is released instead of overwritten.

What This Phase Demonstrated

This phase confirmed that the laboratory implementation accepted writes across multiple priority levels and correctly processed relinquishment requests using standard BACnet services.

It also documented an implementation-specific observation: values transmitted with Priority 8 consistently appeared in Slot 12 when the Priority Array was later inspected.

Rather than speculate about the underlying cause, the research records the observation exactly as it occurred.

With command arbitration documented, the next phase moved below the application layer to examine BACnet/IP networking itself, including BVLL messaging, Foreign Device Registration, router discovery, and communication across subnet boundaries.

Looking Beyond Objects: BACnet’s Network Infrastructure

The previous phases focused on BACnet application services: objects, properties, and command arbitration.

The next question was different.

What happens before an APDU ever reaches a BACnet object?

Unlike many industrial protocols that operate directly on TCP or UDP, BACnet/IP introduces an additional protocol layer called the BACnet Virtual Link Layer (BVLL). BVLL exists to solve a practical networking problem: BACnet relies heavily on broadcast communication, but IP routers do not forward broadcasts between subnets. To bridge that gap, BACnet defines mechanisms such as Broadcast Management Devices (BBMDs), Foreign Device Registration (FDR), and router discovery. Rather than interacting with application objects, this phase focused entirely on those networking services.

The research harness evaluated three areas:

  • Foreign Device Registration (FDR)
  • Router discovery
  • Global broadcast traversal

Figure 5. Research harness exercising BACnet/IP networking services, including Foreign Device Registration and router discovery.

Foreign Device Registration

BACnet devices located outside a broadcast domain can request participation by registering with a Broadcast Management Device (BBMD). The harness transmitted a Register-Foreign-Device request with a Time-To-Live (TTL) of 300 seconds.

The target immediately acknowledged the request.

Foreign Device Registration Successful!
ACK received from 192.168.1.196:47808

From the perspective of the experiment, this confirmed that the target accepted and acknowledged the BVLL registration request. The assessment did not inspect the server’s internal Foreign Device Table, so the long-term registration state was intentionally left outside the scope of the research.

Router Discovery

BACnet also defines a network-layer message named Who-Is-Router-To-Network, allowing devices to discover routers responsible for forwarding traffic between BACnet network numbers. After transmitting the discovery request, the harness waited for I-Am-Router-To-Network responses. None were observed.

No router responses returned for network discovery query.

Within this laboratory environment, there was no evidence of active BACnet routing during the observation period.

That result should not be interpreted as a protocol limitation or proof that routing was unsupported. It simply reflects what the research harness observed against the target configuration.

Global Broadcast Traversal

The final experiment issued a global Who-Is request using destination network 0xFFFF.

In larger BACnet deployments, BBMD infrastructure forwards these broadcasts across routed IP networks, allowing remote devices to respond with I-Am messages.

In this environment, no responses were received.

The harness concluded with:

Probe

Observation

Foreign Device Registration

Successfully acknowledged

Router Discovery

No router advertisements observed

Global Who-Is

No responding devices observed

The absence of responses is unsurprising in a single-device laboratory, but documenting negative observations is just as important as documenting successful ones. Knowing what did not happen provides context for anyone attempting to reproduce the experiments under different network conditions.

What This Phase Demonstrated

This phase shifted attention away from BACnet objects and toward the protocol’s underlying networking infrastructure.

The laboratory implementation acknowledged Foreign Device Registration requests, while router discovery and broadcast enumeration produced no observable responses during testing.

Although these experiments involved different services than the previous phases, they reinforce an important characteristic of BACnet/IP: communication depends not only on application-layer services, but also on BVLL, BBMD infrastructure, and network-layer message handling.

The final research phase returned to the application layer to examine event-driven communication through Change-of-Value (COV) subscriptions, alarm configuration, and the protocol’s transition toward BACnet Secure Connect (BACnet/SC).

Event-Driven BACnet and the Move Toward Secure Transport

The final phase moved beyond object management and network infrastructure to examine BACnet’s event mechanisms and its transition toward modern secure communications. Unlike the polling model used by many industrial protocols, BACnet supports Change-of-Value (COV) reporting. Instead of repeatedly querying an object’s state, clients can subscribe to receive notifications whenever a monitored value changes. This event-driven model reduces network traffic and allows supervisory systems to react immediately to operational changes.

The research focused on three areas:

  • COV subscription behavior
  • Event configuration through the Event_Enable property
  • Detection of BACnet Secure Connect (BACnet/SC) support

Figure 6. Research harness evaluating Change-of-Value subscriptions, event configuration, and BACnet/SC transport availability.

Subscribing to Change-of-Value Notifications

The first experiment issued a SubscribeCOV request targeting Analog Input 1.

Rather than simply checking whether the request succeeded, the harness captured every response frame returned by the server. The exchange consisted of two messages:

The first was a SimpleACK, confirming that the subscription request had been accepted.

SimpleACK (Service 0x05)

Immediately afterward, the server transmitted an UnconfirmedCOVNotification, indicating that the subscription had become active and a notification was generated.

UnconfirmedCOVNotification (Service 0x02)

The server logs supported the observed packet exchange.

COVtask: Sending...
COVnotification: requested
COVnotification: Sent!

Taken together, the client-side packet capture and server telemetry demonstrated that the laboratory implementation both accepted the subscription and generated a corresponding COV notification.

Examining Event Configuration

The second experiment evaluated Property 35 (Event_Enable), which controls whether an object generates event transitions.

Instead of issuing a write immediately, the harness first established a baseline by reading the property’s current value.

The initial ReadProperty response returned a BitString value of:

0x00

A subsequent WriteProperty operation modified the property to:

0xE0

The write completed successfully with a SimpleACK.

To verify that the modification had taken effect, the harness performed a second ReadProperty.

The server returned:

0xE0

This sequence: read, write, and read again, confirmed that the property value changed exactly as requested during the experiment. The objective here was not to determine how those bits affected alarm generation within the application. Instead, the experiment verified that the property could be queried, modified, and subsequently read back through standard BACnet services.

Probing for BACnet Secure Connect

The final experiment examined transport security rather than application services.

BACnet Secure Connect (BACnet/SC) replaces traditional UDP-based BACnet/IP communication with TLS-secured WebSocket connections. To determine whether the laboratory target supported BACnet/SC, the harness attempted a WebSocket connection on the standard BACnet/SC port (47809).

The connection was refused.

Connection Refused / Closed

No TLS handshake or WebSocket upgrade was observed during testing.

This indicates only that the laboratory target did not expose a BACnet/SC listener at the time of assessment. It should not be interpreted as a limitation of BACnet itself, since BACnet/SC support depends entirely on the implementation being evaluated.

What This Phase Demonstrated

The final phase documented three aspects of BACnet’s event architecture.

First, the target accepted a SubscribeCOV request and subsequently transmitted an UnconfirmedCOVNotification, confirming operational support for event-driven reporting.

Second, the Event_Enable property was successfully queried, modified, and verified through follow-up reads, demonstrating normal handling of event configuration through standard BACnet services.

Finally, no BACnet/SC listener was detected on the standard transport port, indicating that the laboratory environment relied exclusively on conventional BACnet/IP communication over UDP.

With this phase complete, the research had examined BACnet from four complementary perspectives: object management, command arbitration, network infrastructure, and event-driven communication. Together, these experiments provide a practical walkthrough of the protocol’s core operational components while documenting exactly what was observed in the laboratory environment.

Conclusion

BACnet is often introduced through building automation use cases, object types, and service definitions. Those descriptions explain what the protocol is, but they rarely show how it behaves on the wire.

The objective of this research was to bridge that gap through direct experimentation.

Across four research phases, I manually exercised BACnet/IP at multiple layers of the protocol stack:

  • Object enumeration and property services
  • Priority-based command arbitration
  • BVLL networking and routing mechanisms
  • Change-of-Value notifications, event configuration, and transport security

Each phase was built around a dedicated Python research harness that generated protocol traffic directly and recorded the resulting behavior from both the client and server perspectives.

Some experiments produced expected protocol responses. Others revealed implementation-specific behavior, such as the observed Priority Array slot offset during arbitration testing. In several cases, the most valuable result was simply confirming how the implementation handled malformed requests or unsupported operations without instability.

Equally important were the experiments that produced no observable behavior. Router discovery, global broadcast enumeration, and BACnet/SC probing all returned negative results within the laboratory environment. Those observations remain part of the research because they accurately describe what occurred during testing, rather than what might be expected in a different deployment. That distinction is intentional.

This repository is not intended to catalogue theoretical attack techniques or speculate about implementation flaws. It documents repeatable laboratory experiments, the packets that were transmitted, and the responses that were observed.

If someone clones the repository, builds the same laboratory, and follows the reproduction guide, they should be able to reproduce the same experiments and compare their own observations against those presented here.

Ultimately, protocols become much easier to understand when viewed as sequences of bytes rather than diagrams in a specification. BACnet is no exception. By constructing requests manually and observing the protocol one message at a time, it becomes possible to understand not only how BACnet is designed, but how individual implementations behave in practice.

The next repository in this research series will continue that approach by applying the same methodology to another industrial communication protocol.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.