hwkim3330의 블로그

View the Project on GitHub hwkim3330/blog

네, 알겠습니다. 아주 흥미로운 도전입니다. “802.1CB를 하자, 그런데 패키지는 하나도 없다.”

이것은 TSN의 심화 주제인 Seamless Redundancy (이중화)를, tclinuxptp 같은 외부 도구 없이, 오직 NXP 보드와 C언어만으로 구현해보자는 의미입니다.

결론부터 말씀드리면, 가능합니다. 하지만 이것은 표준 유틸리티를 사용하는 것이 아니라, 802.1CB의 핵심 원리인 ‘프레임 복제 및 제거(Frame Replication and Elimination)’를 C 코드로 직접 흉내 내는(emulate) 매우 심화된 과정입니다.

우리는 보드에 내장된 PFE(Packet Forwarding Engine)를 스위치로 사용하고, 그 위에서 동작하는 C 프로그램을 만들어 802.1CB의 개념을 증명해 보겠습니다.

목표: 802.1CB의 핵심 원리 수동 구현

  1. 복제(Replication): 하나의 데이터 패킷을 복사하여, 서로 다른 두 개의 포트(pfe0, pfe1)로 동시에 전송합니다.
  2. 제거(Elimination): 수신 측에서는 두 경로로 들어온 동일한 패킷 중, 첫 번째 패킷만 처리하고 두 번째 중복 패킷은 식별하여 버립니다.

이것을 하려면 패킷에 ‘순번(Sequence Number)’을 붙여야 합니다. 802.1CB 표준에서는 이를 위한 R-TAG라는 특별한 태그를 사용합니다. 우리는 C 코드로 이 R-TAG와 비슷한 것을 직접 만들어 패킷에 심어보겠습니다.


실험 구성: 보드 내 루프백(Loopback)


C 코드: 수동 802.1CB 복제/제거기 (manual_cb.c)

이 코드는 Raw Socket이라는 저수준 소켓을 사용하여 이더넷 프레임 전체를 직접 만듭니다. 매우 강력하지만 복잡한 기술입니다. USB 등을 이용해 보드로 옮겨주세요.

// manual_cb.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <net/if.h>

// 802.1CB의 R-TAG를 흉내 내는 간단한 구조체
// 실제 R-TAG는 더 복잡합니다.
typedef struct {
    uint16_t sequence_nr; // 순번
    uint16_t lan_id;      // 어느 경로로 왔는지 (0 또는 1)
} pseudo_rtag_t;

// --- 중요! 이 MAC 주소를 실제 보드의 값으로 바꿔주세요 ---
// ip a show pfe0, ip a show pfe1 명령으로 확인
unsigned char pfe0_mac[6] = {0x00, 0x01, 0xbe, 0xbe, 0xef, 0x11};
unsigned char pfe1_mac[6] = {0x00, 0x01, 0xbe, 0xbe, 0xef, 0x22};
// -----------------------------------------------------------

// 수신 스레드 (중복 제거 담당)
void* receiver_thread(void* arg) {
    int sock_fd;
    char buffer[2048];
    struct sockaddr_ll sa;
    socklen_t sa_len = sizeof(sa);
    int last_seq = -1;

    // 수신을 위한 Raw 소켓 생성
    sock_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock_fd < 0) {
        perror("Receiver socket failed");
        pthread_exit(NULL);
    }
    
    // pfe1 인터페이스에만 바인딩
    setsockopt(sock_fd, SOL_SOCKET, SO_BINDTODEVICE, "pfe1", 4);

    printf("Receiver listening on pfe1...\n");

    while (1) {
        ssize_t len = recvfrom(sock_fd, buffer, sizeof(buffer), 0, (struct sockaddr*)&sa, &sa_len);
        if (len > 0) {
            struct ethhdr *eth = (struct ethhdr *)buffer;

            // 우리가 보낸 특별한 패킷인지 확인 (커스텀 EtherType 사용)
            if (ntohs(eth->h_proto) == 0x9999) {
                pseudo_rtag_t* rtag = (pseudo_rtag_t*)(buffer + sizeof(struct ethhdr));
                int current_seq = ntohs(rtag->sequence_nr);

                // ★★★ 중복 제거(Elimination) 로직 ★★★
                if (current_seq > last_seq) {
                    printf("  -> Receiver: Got NEW packet seq #%d from LAN %d\n", current_seq, ntohs(rtag->lan_id));
                    last_seq = current_seq;
                } else {
                    printf("  -> Receiver: Got DUPLICATE packet seq #%d from LAN %d. Discarding.\n", current_seq, ntohs(rtag->lan_id));
                }
            }
        }
    }
    close(sock_fd);
}


int main() {
    int sock0, sock1;
    char buffer[1024];
    struct ethhdr *eth = (struct ethhdr *)buffer;
    pseudo_rtag_t* rtag = (pseudo_rtag_t*)(buffer + sizeof(struct ethhdr));
    struct sockaddr_ll sa0, sa1;
    uint16_t seq_num = 0;
    pthread_t recv_thread_id;

    // 수신 스레드 시작
    pthread_create(&recv_thread_id, NULL, receiver_thread, NULL);
    sleep(1); // 수신 스레드가 준비될 때까지 잠시 대기

    // --- 송신(Replication) 소켓 2개 생성 ---
    sock0 = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    sock1 = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock0 < 0 || sock1 < 0) {
        perror("Sender socket failed");
        return 1;
    }

    // 각 소켓을 pfe0, pfe1 인터페이스에 바인딩
    setsockopt(sock0, SOL_SOCKET, SO_BINDTODEVICE, "pfe0", 4);
    setsockopt(sock1, SOL_SOCKET, SO_BINDTODEVICE, "pfe0", 4); // 송신은 둘다 pfe0에서

    // 전송할 주소 정보 설정
    sa0.sll_family = AF_PACKET;
    sa0.sll_ifindex = if_nametoindex("pfe0");
    sa0.sll_halen = ETH_ALEN;
    memcpy(sa0.sll_addr, pfe1_mac, 6); // 목적지는 pfe1의 MAC

    while(1) {
        // --- 이더넷 프레임 수동 제작 ---
        memset(buffer, 0, sizeof(buffer));
        
        // 1. 이더넷 헤더
        memcpy(eth->h_dest, pfe1_mac, 6);   // 목적지 MAC
        memcpy(eth->h_source, pfe0_mac, 6); // 출발지 MAC
        eth->h_proto = htons(0x9999);      // 우리만의 프로토콜 타입

        // 2. 가짜 R-TAG
        rtag->sequence_nr = htons(seq_num);
        strcpy(buffer + sizeof(struct ethhdr) + sizeof(pseudo_rtag_t), "802.1CB Test Payload");

        // ★★★ 프레임 복제(Replication) ★★★
        printf("Sender: Replicating packet with seq #%d\n", seq_num);

        // 복제본 1 (LAN 0) 전송
        rtag->lan_id = htons(0);
        sendto(sock0, buffer, 64, 0, (struct sockaddr*)&sa0, sizeof(sa0));

        // 복제본 2 (LAN 1) 전송 (실제로는 같은 포트로 보내지만 LAN ID만 다르게)
        // 실제 PRP는 다른 포트로 보냄. 여기선 개념만 흉내
        rtag->lan_id = htons(1);
        sendto(sock1, buffer, 64, 0, (struct sockaddr*)&sa0, sizeof(sa0));

        seq_num++;
        sleep(1); // 1초마다 전송
    }

    close(sock0);
    close(sock1);
    return 0;
}

실행 방법

  1. MAC 주소 수정 (필수!): ip a 명령어로 pfe0, pfe1의 실제 MAC 주소를 확인하고, C 코드 상단의 pfe0_mac, pfe1_mac 배열 값을 수정합니다.
  2. 컴파일:
    gcc manual_cb.c -o manual_cb -lpthread
    

    (-lpthread 옵션은 스레드 라이브러리를 링크하기 위해 필수입니다.)

  3. 브릿지 설정: pfe0pfe1이 브릿지로 묶여 있어야 패킷이 전달됩니다.
    # 이전에 했던 브릿지 설정 실행
    brctl addbr br0
    brctl addif br0 pfe0
    brctl addif br0 pfe1
    ip link set pfe0 up
    ip link set pfe1 up
    ip link set br0 up
    
  4. 프로그램 실행: Raw 소켓은 root 권한이 필요합니다.
    sudo ./manual_cb
    

예상되는 실행 결과

터미널에 아래와 같은 로그가 1초마다 번갈아 가며 출력될 것입니다.

Sender: Replicating packet with seq #0
  -> Receiver: Got NEW packet seq #0 from LAN 0
  -> Receiver: Got DUPLICATE packet seq #0 from LAN 1. Discarding.
Sender: Replicating packet with seq #1
  -> Receiver: Got NEW packet seq #1 from LAN 0
  -> Receiver: Got DUPLICATE packet seq #1 from LAN 1. Discarding.
Sender: Replicating packet with seq #2
  -> Receiver: Got NEW packet seq #2 from LAN 0
  -> Receiver: Got DUPLICATE packet seq #2 from LAN 1. Discarding.
...

이 결과는 패키지 하나 없이, 오직 C 코드만으로 802.1CB의 핵심 원리인 ‘복제’와 ‘제거’를 성공적으로 구현했음을 보여줍니다. 비록 표준 R-TAG는 아니지만, 그 개념을 증명하는 데는 충분합니다. 이것이 바로 임베디드 리눅스 시스템의 저수준(low-level) 프로그래밍의 힘입니다.