# Optional Fields issue

**URL:** <https://community.zeek.org/t/optional-fields-issue/7798>\
**Category:** Zeek\
**Tags:** spicy\
**Created:** [June 5, 2025, 4:26pm UTC](https://community.zeek.org/t/optional-fields-issue/7798 "2025-06-05T16:26:41Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Nourgr23](https://avatars.discourse-cdn.com/v4/letter/n/5daacb/32.png) [@Nourgr23](https://community.zeek.org/u/Nourgr23)\
**Post date:** [June 5, 2025, 4:26pm UTC](https://community.zeek.org/t/optional-fields-issue/7798/1 "2025-06-05T16:26:41Z")

</div>

Hello everyone,

I’m currently working on developing a Zeek packet analyzer for **ProfinetIO** as part of my Master’s thesis.

**1** -From my understanding, the real-time data exchange in Profinet is not built on traditional transport layer protocols like TCP/UDP, which is why creating a **packet analyzer** seemed like the right approach.

**2** -I’ve taken inspiration from(_[GitHub - kit-dsn/zeek-profinet-analyzer: Profinet packet analyzer plugin for Zeek.](https://github.com/kit-dsn/zeek-profinet-analyzer)_) , and my goal is to refine the parser to focus on the most important features that need to be logged. The Spicy `ProfinetIO.spicy` parser seems to produce the correct results when parsing the packets. However, Zeek is not generating the expected `profinet_dcp.log` file.

The issue appears specifically when handling **DCP Identify Request/Response packets**. In the `analyzer.log`, Zeek reports the following error:

```auto
unset optional value

```

These are the scripts:  
**#ProfinetIO.spicy:**

```auto
module ProfinetIO;

import spicy;

%byte-order = spicy::ByteOrder::Network;

public type Packet = unit {
	frame_id: uint16;
	rtc_frame: RTCFrame if ( 0x8000 <= self.frame_id && self.frame_id <= 0xbbff );
	dcp_frame: DCPFrame if ( 0xfefd <= self.frame_id && self.frame_id <= 0xfeff );
            data: bytes &eod; # Any remaining bytes (optional)
            on %done {print self;}  
};

type RTCFrame = unit {
    # RTC Data (adjust size to match your observed packets)
    data: bytes &size=40; # 40-byte data block (update if needed)
    cycle_counter: uint16; # Cycle counter (2 bytes)
    data_status: uint8; # Data Status (1 byte)
    transfer_status: uint8; # Transfer Status (1 byte)
};

type ServiceIDType = enum {
	GET = 0x01,
	SET = 0x02,
	IDENTIFY = 0x05,
	HELLO = 0x06
};

type DCPFrame = unit {
	service_id: uint8; # &convert=ServiceIDType($$);
	service_type: bitfield(8) {
		stype: 0..1;
		success: 2;
		};
	xid: uint32;
	response_delay: uint16;
	len: uint16 { self.rest = $$; }
	blocks: DCPBlock(self)[] &until-including=(self.rest == 0);

	var rest: uint16 = 0;
};

type DCPBlock = unit(inout frame: DCPFrame) {
    option: uint8;
    suboption: uint8;
    len: uint16 { frame.rest -= ($$ + 4); }
    BlockInfo: uint16 if (frame.service_type.stype == 1);

    ip_params: IPParams if (self.option == 1 && self.suboption == 2);
    vendor_info: VendorInfo(self.len) if (self.option == 2 && self.suboption == 1);
    station_name: StationName(self.len)if (self.option == 2 && self.suboption == 2);
    device_id: DeviceID if (self.option == 2 && self.suboption == 3);
    device_role: DeviceRole if (self.option == 2 && self.suboption == 4);
    data: bytes &size=self.len - 2
        if (!((self.option == 1 && self.suboption == 2) ||
             (self.option == 2 && self.suboption == 1) ||
             (self.option == 2 && self.suboption == 2) ||
             (self.option == 2 && self.suboption == 3) ||
             (self.option == 2 && self.suboption == 4) ||
             (self.len == 0)));
             
    padding: int8 if (self.len % 2 == 1);
    on padding { frame.rest -= 1; }
};

type IPParams = unit {
    IPaddress: addr &ipv4; # 4 bytes
    Subnetmask: addr &ipv4; # 4 bytes
    StandardGateway: addr &ipv4; # 4 bytes
};

type VendorInfo = unit(len: uint16) {
    DeviceVendorValue: bytes &size=len - 2; 
};

type StationName = unit(len: uint16) {
    NameOfStation: bytes &size=len - 2; 
};

type DeviceID = unit {
    vendor_id: uint16;
    device_id: uint16;
};

type DeviceRole = unit {
    DeviceRoleDetails: uint8;
    Reserved: uint8;
};

```

**#Zeek\_ProfinetIO.spicy:**

```auto
module Zeek_ProfinetIO;

import ProfinetIO;
import spicy;
import zeek;

# Converts a ProfinetIO::DCPFrame into a Zeek type.
public function create_dcp_msg(dcp: ProfinetIO::DCPFrame):
    tuple<
        uint8,
        bool,
        bool,
        bool,
        uint32,
        uint16> {
    return (
        dcp.service_id,
        dcp.service_type.stype == 0,
        dcp.service_type.stype == 1,
        dcp.service_type.success == 0,
        dcp.xid,
        dcp.response_delay
    );
}

# Converts a ProfinetIO::DCPBlock into a Zeek type.
public function create_dcp_block(block: ProfinetIO::DCPBlock):
    tuple<
        uint8,
        uint8,
        uint16,
        bytes,
        addr,
        addr,
        addr,
        bytes,
        uint16,
        uint16,
        bytes> { 
    return (
        block.option,
        block.suboption,
        block.len,
        block?.station_name ? block.station_name.NameOfStation : b"",
        block?.ip_params ? block.ip_params.IPaddress : 0.0.0.0,
        block?.ip_params ? block.ip_params.Subnetmask : 0.0.0.0,
        block?.ip_params ? block.ip_params.StandardGateway : 0.0.0.0,
        block?.vendor_info ? block.vendor_info.DeviceVendorValue : b"",
        block?.device_id ? block.device_id.vendor_id : 0,
        block?.device_id ? block.device_id.device_id : 0,
        block?.data ? block.data : b""
    );
}
# Converts a ProfinetIO::RTCFrame into a Zeek type.
public function create_rtc_msg(rtc: ProfinetIO::RTCFrame):
    tuple<
        bytes,
        uint16,
        uint8,
        uint8> {
    return (
        rtc.data,
        rtc.cycle_counter,
        rtc.data_status,
        rtc.transfer_status
    );
}

```

**#ProfinetIO.evt**

```auto
packet analyzer spicy::ProfinetIO:
	parse with ProfinetIO::Packet;

import Zeek_ProfinetIO;

on ProfinetIO::Packet::dcp_frame -> event ProfinetIO::dcp_message(self.frame_id, Zeek_ProfinetIO::create_dcp_msg(self.dcp_frame), [Zeek_ProfinetIO::create_dcp_block(block) for block in self.dcp_frame.blocks]);

on ProfinetIO::Packet::rtc_frame -> event ProfinetIO::rtc_message(self.frame_id, Zeek_ProfinetIO::create_rtc_msg(self.rtc_frame));

```

**#ProfinetIO.zeek**

```auto
@load ./ProfinetIO.hlto
module ProfinetIO;

export {
    redef enum Log::ID += { LOG_DCP, LOG_RTC };

    # DCP Header Structure
    type DCPHeader: record {
        service_id: count;
        request: bool;
        response: bool;
        success: bool;
        xid: count;
        response_delay: count;
    };

    type DCPBlock: record {
        opt: count;
        subopt: count;
        len: count;
        NameOfStation: string;
        IPaddress: addr;
        Subnetmask: addr;
        StandardGateway: addr;
        DeviceVendorValue: string;
        VendorID: count;
        DeviceID: count;
        data: string;
    };

    type DCPInfo: record {
        ts: time &log;
        frame_id: count &log;
        service_id: count &log;
        request: bool &log;
        response: bool &log;
        success: bool &log;
        xid: count &log;
        response_delay: count &log;
        NameOfStation: string &log;
        IPaddress: addr &log;
        Subnetmask: addr &log;
        StandardGateway: addr &log;
        DeviceVendorValue: string &log;
        VendorID: count &log;
        DeviceID: count &log;
        data: string &log;
    };

    # RTC Header Structure
    type RTCHeader: record {
        data: string;
        cycle_counter: count;
        data_status: count;
        transfer_status: count;
    };

    # RTC Log Structure
    type RTCInfo: record {
        ts: time &log;
        frame_id: count &log;
        cycle_counter: count &log;
        data_status: count &log;
        transfer_status: count &log;
    };
}

event zeek_init() &priority=10
    {
    PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_ETHERNET, 0x8892, PacketAnalyzer::ANALYZER_SPICY_PROFINETIO);
	PacketAnalyzer::register_packet_analyzer(PacketAnalyzer::ANALYZER_VLAN, 0x8892, PacketAnalyzer::ANALYZER_SPICY_PROFINETIO);
    # Create logging streams
    Log::create_stream(ProfinetIO::LOG_DCP, [$columns=DCPInfo, $path="profinet_dcp"]);
    Log::create_stream(ProfinetIO::LOG_RTC, [$columns=RTCInfo, $path="profinet_rtc"]);
    }

# Handle DCP messages
event ProfinetIO::dcp_message(frame_id: count, hdr: DCPHeader, blocks: vector of DCPBlock)
    {
    local NameOfStation: string;
    local IPaddress: addr;
    local Subnetmask: addr;
    local StandardGateway: addr;
    local DeviceVendorValue: string;
    local VendorID: count;
    local DeviceID: count;
    local data: string;

    for (i in blocks)
        {
        local block = blocks[i];

        if (block?$NameOfStation && block$NameOfStation != "")
            NameOfStation = block$NameOfStation;
        if (block?$IPaddress && block$IPaddress != 0.0.0.0)
            IPaddress = block$IPaddress;
        if (block?$Subnetmask && block$Subnetmask != 0.0.0.0)
            Subnetmask = block$Subnetmask;
        if (block?$StandardGateway && block$StandardGateway != 0.0.0.0)
            StandardGateway = block$StandardGateway;
        if (block?$DeviceVendorValue && block$DeviceVendorValue != "")
            DeviceVendorValue = block$DeviceVendorValue;
        if (block?$VendorID && block$VendorID != 0)
            VendorID = block$VendorID;
        if (block?$DeviceID && block$DeviceID != 0)
            DeviceID = block$DeviceID;
        if (block?$data && block$data != "")
            data = block$data;
        }
        

    local rec: DCPInfo = [
        $ts = network_time(),
        $frame_id = frame_id,
        $service_id = hdr$service_id,
        $request = hdr$request,
        $response = hdr$response,
        $success = hdr$success,
        $xid = hdr$xid,
        $response_delay = hdr$response_delay,
        $NameOfStation = NameOfStation,
        $IPaddress = IPaddress,
        $Subnetmask = Subnetmask,
        $StandardGateway= StandardGateway,
        $DeviceVendorValue = DeviceVendorValue,
        $VendorID = VendorID,
        $DeviceID = DeviceID,
        $data = data,
    ]; 

    Log::write(LOG_DCP, rec);
    }

# Handle RTC messages
event ProfinetIO::rtc_message(frame_id: count, hdr: RTCHeader)
    {
    local rec: RTCInfo = [
        $ts = network_time(),
        $frame_id = frame_id,
        $cycle_counter = hdr$cycle_counter,
        $data_status = hdr$data_status,
        $transfer_status = hdr$transfer_status
    ];

    Log::write(LOG_RTC, rec);
    }

```

This seems to be related to optional attributes used in DCP blocks. I’ve tried several approaches but unfortunately, none of these attempts have resolved the issue.

**3** -Additionally, if you have any suggestions on how to integrate the parsed Profinet traffic into _ **conn.log** _ or associate it with connection metadata, that would be very helpful.

This is my first experience working with Zeek and Spicy, and it’s a crucial component of my research. I’d be very thankful for your insights or advice.

@Benjamin_Bannier I will be very grateful if you could help me as soon as possible. Any suggestions or feedback from the community would be greatly appreciated. Thanks everyone in advance!

---

<div class="post-metadata">

**Author:** ![Benjamin\_Bannier](https://yyz1.discourse-cdn.com/flex011/user_avatar/community.zeek.org/benjamin_bannier/32/595_2.png) [@Benjamin\_Bannier](https://community.zeek.org/u/Benjamin_Bannier)\
**Post date:** [June 6, 2025, 8:07am UTC](https://community.zeek.org/t/optional-fields-issue/7798/2 "2025-06-06T08:07:35Z")

</div>

> [@Nourgr23](#):
>
> `unset optional value`

The actual error should also include a Spicy source location so one can see where the unset value was accessed. Depending on the actual value there, this could either be [dereferencing](https://docs.zeek.org/projects/spicy/en/latest/programming/language/types.html#operator-optional::Deref) of an `optional<T>`, or access to a field marked `&optional`. All `unit` fields are implicitly marked `&optional` and one cannot access their value before they are parsed (hopefully makes sense 😅); in the EVT file in your code above you e.g., invoke field hooks when a field is parsed, so most of the time this just works, but one can check whether a field has a value with [`?.`](https://docs.zeek.org/projects/spicy/en/latest/programming/language/types.html#operator-struct::HasMember) if needed.
