文章

vsomeip的e2e研究

E2E 介绍

AUTOSAR(AUTomotive Open System ARchitecture)是一个开放和标准化的软件架构,它用于汽车行业中的电子控制单元(ECU)开发。为了确保数据通信的可靠性和安全性,AUTOSAR引入了End-to-End(E2E)通信保护机制。E2E保护的目的是在数据传输过程中检测和应对可能出现的错误,如数据损坏、丢失或重放攻击。

E2E保护的基本概念

E2E保护通过在数据报文中添加校验信息(如CRC校验、序列号等)来检测传输中的错误。它的核心理念是即使数据在传输链的某个环节出现了错误,接收端也能通过这些校验信息检测到,并采取相应的措施。

VSOMEIP 中的E2E

在VSOMEIP的v3.1.20.3版本中,只支持三种E2E:p01、p04和custom。在VSOMEIP中如果要启用E2E则需要在VSOMEIP的配置文件中进行配置。E2E的配置分为protectorchecker以及bothprotector是对数据进行保护,checker是对数据进行检测,both是同时支持两种。下面给出P04的E2E配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// protect
"e2e" :
{
    "e2e_enabled" : "true",
    "protected" :
    [
        {
            "service_id" : "0xd025",
            "event_id" : "0x0001",
            "profile" : "P04",
            "variant" : "protector",
            "crc_offset" : "64",
            "data_id" : "0x2d"
        }
    ]
}

// check
"e2e" :
{
    "e2e_enabled" : "true",
    "protected" :
    [
        {
            "service_id" : "0xd025",
            "event_id" : "0x0001",
            "profile" : "P04",
            "variant" : "checker",
            "crc_offset" : "64",
            "data_id" : "0x2d"
        }
    ]
}

VSOMEIP在e2e_enabled为true的情况下,会加载e2e模块。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在routing_manager_impl::init函数中
if( configuration_->is_e2e_enabled()) { // 判断e2e是否开启,即配置文件中的e2e_enabled
    VSOMEIP_INFO << "E2E protection enabled.";

    const char *its_e2e_module = getenv(VSOMEIP_ENV_E2E_PROTECTION_MODULE);
    std::string plugin_name = its_e2e_module != nullptr ? its_e2e_module : VSOMEIP_E2E_LIBRARY;

    // 加载e2e模块
    auto its_plugin = plugin_manager::get()->get_plugin(plugin_type_e::APPLICATION_PLUGIN, plugin_name);
    if (its_plugin) {
        VSOMEIP_INFO << "E2E module loaded.";
        e2e_provider_ = std::dynamic_pointer_cast<e2e::e2e_provider>(its_plugin);
    }
}

发送数据的数据,对数据进行E2E保护的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// bool routing_manager_impl::send(client_t _client, const byte_t *_data,
//         length_t _size, instance_t _instance, bool _reliable,
//         client_t _bound_client,
//         credentials_t _credentials,
//         uint8_t _status_check, bool _sent_from_remote)
// ......
if (e2e_provider_) {
    if ( !is_service_discovery) {
        service_t its_service = VSOMEIP_BYTES_TO_WORD(
                _data[VSOMEIP_SERVICE_POS_MIN], _data[VSOMEIP_SERVICE_POS_MAX]);
        method_t its_method = VSOMEIP_BYTES_TO_WORD(
                _data[VSOMEIP_METHOD_POS_MIN], _data[VSOMEIP_METHOD_POS_MAX]);
#ifndef ANDROID
        // is_protected会判断该消息是否需要保护,判断的依据就是配置文件中有没有进行配置
        if (e2e_provider_->is_protected({its_service, its_method})) {
            // Find out where the protected area starts
            // get_protection_base会得到someip的基础头,e2e会跳过此部分,e2e会对接下来的数据进行protect。
            size_t its_base = e2e_provider_->get_protection_base({its_service, its_method});

            // Build a corresponding buffer
            // 跳过基础头
            its_buffer.assign(_data + its_base, _data + _size);

            // 对数据进行保护
            e2e_provider_->protect({ its_service, its_method }, its_buffer, _instance);

            // Prepend header
            its_buffer.insert(its_buffer.begin(), _data, _data + its_base);

            _data = its_buffer.data();
        }
#endif
    }
}

具体的protect代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 具体的是P04的protect
void
protector::protect(e2e_buffer &_buffer, instance_t _instance) {
    std::lock_guard<std::mutex> lock(protect_mutex_);

    if (_instance > VSOMEIP_E2E_PROFILE04_MAX_INSTANCE) {
        VSOMEIP_ERROR << "E2E Profile 4 can only be used for instances [1-255]";
        return;
    }

    /** @req: [SWS_E2E_00363] */
    if (verify_inputs(_buffer)) {

        /** @req [SWS_E2E_00364] */
        // 写入16位的length
        write_16(_buffer, static_cast<uint16_t>(_buffer.size()), 0);

        /** @req [SWS_E2E_00365] */
        // 写入16位的counter
        write_16(_buffer, counter_, 2);

        /** @req [SWS_E2E_00366] */
        // 写入data_id
        uint32_t its_data_id(uint32_t(_instance) << 24 | config_.data_id_);
        write_32(_buffer, its_data_id, 4);

        /** @req [SWS_E2E_00367] */
        // 计算crc
        uint32_t its_crc = profile_04::compute_crc(config_, _buffer);

        /** @req [SWS_E2E_0368] */
        // 写入crc
        write_32(_buffer, its_crc, 8);

        /** @req [SWS_E2E_00369] */
        increment_counter();
    }
}

可以看到会对12个字节进行覆盖,所以实际发送的时候需要预留12字节的空间。配置文件中的crc_offset的作用是写入P04E2E的偏移量,具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// config_.offset_就是配置文件中crc_offest的值
void
protector::write_16(e2e_buffer &_buffer, uint16_t _data, size_t _index) {

    _buffer[config_.offset_ + _index] = VSOMEIP_WORD_BYTE1(_data);
    _buffer[config_.offset_ + _index + 1] = VSOMEIP_WORD_BYTE0(_data);
}

void
protector::write_32(e2e_buffer &_buffer, uint32_t _data, size_t _index) {

    _buffer[config_.offset_ + _index] = VSOMEIP_LONG_BYTE3(_data);
    _buffer[config_.offset_ + _index + 1] = VSOMEIP_LONG_BYTE2(_data);
    _buffer[config_.offset_ + _index + 2] = VSOMEIP_LONG_BYTE1(_data);
    _buffer[config_.offset_ + _index + 3] = VSOMEIP_LONG_BYTE0(_data);
}

收到数据会进行E2E的检测,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// void routing_manager_impl::on_message(const byte_t *_data, length_t _size,
//         endpoint *_receiver, const boost::asio::ip::address &_destination,
//         client_t _bound_client, credentials_t _credentials,
//         const boost::asio::ip::address &_remote_address,
//         std::uint16_t _remote_port)
// .......

// is_checked会判断该消息是否需要保护,判断的依据就是配置文件中有没有进行配置
if (e2e_provider_->is_checked({its_service, its_method})) {
    auto its_base = e2e_provider_->get_protection_base({its_service, its_method});
    e2e_buffer its_buffer(_data + its_base, _data + _size);
    e2e_provider_->check({its_service, its_method},
            its_buffer, its_instance, its_check_status);

    if (its_check_status != e2e::profile_interface::generic_check_status::E2E_OK) {
        VSOMEIP_INFO << "E2E protection: CRC check failed for service: "
                << std::hex << its_service << " method: " << its_method;
    }
}

然后是具体的check代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 具体的是P04的check
if (verify_input(_buffer)) {
    /** @req [SWS_E2E_357] */
    uint16_t its_received_length;
    if (read_16(_buffer, its_received_length, 0)) {
        /** @req [SWS_E2E_358] */
        uint16_t its_received_counter;
        if (read_16(_buffer, its_received_counter, 2)) {
            /** @req [SWS_E2E_359] */
            uint32_t its_received_data_id;
            if (read_32(_buffer, its_received_data_id, 4)) {
                /** @req [SWS_E2E_360] */
                uint32_t its_received_crc;
                if (read_32(_buffer, its_received_crc, 8)) {
                    uint32_t its_crc = profile_04::compute_crc(config_, _buffer);
                    /** @req [SWS_E2E_361] */
                    if (its_received_crc != its_crc) {
                        _generic_check_status = e2e::profile_interface::generic_check_status::E2E_WRONG_CRC;
                        VSOMEIP_ERROR << std::hex << "E2E P04 protection: CRC32 does not match: calculated CRC: "
                                << its_crc << " received CRC: " << its_received_crc;
                    } else {
                        uint32_t its_data_id(uint32_t(_instance) << 24 | config_.data_id_);
                        if (its_received_data_id == its_data_id
                                && static_cast<size_t>(its_received_length) == _buffer.size()
                                && verify_counter(its_received_counter)) {
                        _generic_check_status = e2e::profile_interface::generic_check_status::E2E_OK;
                        }
                        counter_ = its_received_counter;
                    }
                }
            }
        }
    }
}

依次获得length、counter、data_id以及crc,然后用提取的crc和这边计算的crc进行对比,如果一样,还需要验证data_id和couter_id。用户是通过调用message::is_valid_crc来判断E2E是否检测成功的,vsomeip在上面的代码中进行了check后,需要将check结果返回给用户,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool routing_manager_impl::deliver_message(const byte_t *_data, length_t _size,
        instance_t _instance, bool _reliable, client_t _bound_client, credentials_t _credentials,
        uint8_t _status_check, bool _is_from_remote) {
    bool is_delivered(false);
    std::uint32_t its_sender_uid = std::get<0>(_credentials);
    std::uint32_t its_sender_gid = std::get<1>(_credentials);

    auto its_deserializer = get_deserializer();
    its_deserializer->set_data(_data, _size);
    std::shared_ptr<message_impl> its_message(its_deserializer->deserialize_message());
    its_deserializer->reset();
    put_deserializer(its_deserializer);

    if (its_message) {
        its_message->set_instance(_instance);
        its_message->set_reliable(_reliable);
        its_message->set_check_result(_status_check);
        its_message->set_uid(std::get<0>(_credentials));
        its_message->set_gid(std::get<1>(_credentials));
//  ......

在deliver_message函数中会设置检查的结果,然后用户就可以调用 message::is_valid_crc() 判断是否e2e检测成功。

e2e除了p04,还支持p01,p01的配置文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"e2e_enabled" : "true",
"protected" :
[
    {
        "service_id" : "0x1234",
        "event_id" : "0x8421",
        "profile" : "CRC8",
        "variant" : "checker",
        "crc_offset" : "0",
        "data_id_mode" : "3",
        "data_length" : "56",
        "data_id" : "0xA73"
    }
]

可以看到p01多,data_id_mode,data_id_mode就是将data_id纳入crc计算的方式有以下几种: 这段描述定义了四种将两字节数据 ID 纳入一字节 CRC 计算的模式。具体如下:

  1. E2E_P01_DATAID_BOTH:
    • 在这种模式下,两个字节(双字节 ID 配置)都被包含在 CRC 计算中,先低字节,再高字节(参见变体 1A - PRS_E2EProtocol_00227)。
  2. E2E_P01_DATAID_ALT:
    • 根据计数器的奇偶性(交替 ID 配置),高字节或低字节会被包含在 CRC 计算中(参见变体 1B - PRS_E2EProtocol_00228)。对于偶数计数器值,低字节被包含;对于奇数计数器值,高字节被包含。
  3. E2E_P01_DATAID_LOW:
    • 只有低字节被包含在 CRC 计算中,高字节从未使用。这相当于数据 ID(在给定应用中)只有 8 位的情况。
  4. E2E_P01_DATAID_NIBBLE:
    • 数据 ID 的高字节的高 4 位(高半字节)不使用(它是 0x0),因为数据 ID 限制为 12 位。
    • 数据 ID 的高字节的低 4 位(低半字节)会显式传输,并且在计算数据的 CRC 时被覆盖。
    • 低字节不会被传输,但它作为初始值被包含在 CRC 计算中(隐式传输,就像 _BOTH、_ALT 和 _LOW 模式一样)。

crc_offest在p04中为开始插入e2e的位置,在p01中较为复杂,仅仅是crc的offset,p01中还存在 counter offset data_id nibble offset。这里没有在配置文件中给出,则默认值分别为8和12。

而custom的e2e的配置则是如下:

1
2
3
4
5
6
7
{
    "service_id" : "0x1234",
    "event_id" : "0x6543",
    "profile" : "CRC32",
    "variant" : "checker",
    "crc_offset" : "0"
}

profile为CRC32,此时为custum profile,custum profile和没有data_id,counter_id等,只需要对数据进行crc32即可,存放的位置和crc_offest有关。

本文由作者按照 CC BY 4.0 进行授权