- Linux Kernel 4.6.3 /net/ethernet/eth.c -


[ /net/ethernet/eth.c - Function 'eth_header' ]


이하의 코드는 eth_header함수의 원형이다.


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
int eth_header(struct sk_buff *skb, struct net_device *dev,
           unsigned short type,
           const void *daddr, const void *saddr, unsigned int len)
{
    struct ethhdr *eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);
 
    if (type != ETH_P_802_3 && type != ETH_P_802_2)
        eth->h_proto = htons(type);
    else
        eth->h_proto = htons(len);
 
    if (!saddr)
        saddr = dev->dev_addr;
    memcpy(eth->h_source, saddr, ETH_ALEN);
 
    if (daddr) {
        memcpy(eth->h_dest, daddr, ETH_ALEN);
        return ETH_HLEN;
    }
 
    if (dev->flags & (IFF_LOOPBACK | IFF_NOARP)) {
        eth_zero_addr(eth->h_dest);
        return ETH_HLEN;
    }
 
    return -ETH_HLEN;
}
EXPORT_SYMBOL(eth_header);
cs

[ eth.c - eth_header(...) ]


먼저 매개변수 분석부터 시작하자. 차례대로 skb, dev, type, daddr, saddr, len을 넘겨받고 있다.

skb는 sk_buff*형으로, 이더넷 헤더를 구현할 소켓 버퍼를 가리키고 있는 포인터이다.

dev는 패킷 처리를 위해 참조할 네트워크 디바이스 구조체를 가리키는 포인터.

type은 상위 프로토콜 타입을 나타내고, daddr과 saddr은 각각 Destination과 Source H/W Address를 나타내게 된다. 마지막으로 len은 패킷 길이를 나타내는 매개변수이다.

다음으로는, 루틴을 분석하도록 하겠다. 위 루틴에서 가장 첫 줄에서 ethhdr*형 eth에 skb_push의 리턴 값을 ethhdr*형으로 형 변환해서 대입하고 있는데,  skb_push의 경우 sk_buff를 인자로 받고 있다. 해당 함수는 skbuff.c에 명세 되어 있으며, 간략하게 알아보자면. skb_push함수는 내부적으로 다음과 같은 루틴을 수행하게 된다.


1
2
3
4
5
6
7
8
9
unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
{
    skb->data -= len;
    skb->len  += len;
    if (unlikely(skb->data<skb->head))
        skb_under_panic(skb, len, __builtin_return_address(0));
    return skb->data;
}
EXPORT_SYMBOL(skb_push);
cs

[ skbuff.c - skb_push(...) ]


매개변수로는 sk_buff*형인 skb와 unsigned int형인 len을 받게 되며, 매개변수로 받은 skb의 data멤버(실 버퍼의 시작 위치 포인터)를 len만큼 빼게 되고 (기존 버퍼의 앞쪽에 len만큼 추가적인 공간을 확보), 해당 연산을 통해 확보한 공간을 len멤버(실 버퍼의 크기를 나타내는 멤버)에 더해주게 된다.

정리하자면, eth_header함수의 코드에서는

struct ethhdr *eth = (struct ethhdr *)skb_push(skb, ETH_HLEN);

를 호출하고 있는데 이를 skb_push()함수의 동작과 함께 정리하자면,

'sk_buff* skb를 skb_push함수에 인자로 넘겨 ETH_HLEN(이더넷 헤더 크기)만큼 추가적인 공간을 버퍼의 앞 부분에 추가적으로 확보한 뒤, 해당 버퍼 포인터를 ethhdr*형(이더넷 헤더 포인터형)으로 변환해서 eth에 저장하게 된다.

여기서 ethhdr구조체가 어떤 구조로 이루어져 있는지 짚고 넘어가도록 하겠다.


1
2
3
4
5
6
struct ethhdr {
        unsigned char   h_dest[ETH_ALEN];       /* destination eth addr */
        unsigned char   h_source[ETH_ALEN];     /* source ether addr    */
        __be16          h_proto;                /* packet type ID field */
} __attribute__((packed));
 
cs

[ if_ether.h - struct ethhdr ]


ethhdr은 다음과 같은 구조로 되어 있다. 여기서 ETH_ALEN은 #define ETH_ALEN 6으로서 정의되어 있다. 따라서 Octet * 6으로서, ETH_ALEN크기의 uchar배열 h_dest와 h_source는 H/W주소를 나타냄을 알 수 있다. 그 하단에 h_proto는 프로토콜 지정자로서 __be16 (u16 - unsigned short)형으로서, 일반적으로 2Byte를 나타낸다. 이를 실제 이더넷 패킷과 비교하면,



0~13 (Byte)Offset까지 존재하는 EthernetII 헤더와 1 : 1대응이 된다는 것을 알 수 있다.

계속 진행을 해서 다음 코드 부분을 분석해보도록 하겠다.


1
2
3
4
    if (type != ETH_P_802_3 && type != ETH_P_802_2)
        eth->h_proto = htons(type);
    else
        eth->h_proto = htons(len);
cs

[ eth.c - eth_header(...) ]


위 코드에서는 매개변수로 넘겨 받은 type에 들어있는 값이 ETH_P_802_3, ETH_P_802_2 둘 중 어느 것과도 같지 않을 경우, type을 그대로 ethhdr구조체 eth의 h_proto멤버에 대입하게 된다. 즉, 일반적인 EthernetII 헤더와 같이 type값을 넣는다는 것을 알 수 있다.

그러나 type이 ETH_P_802_3 혹은, ETH_P_802_2일 경우에는 h_proto에 len을 넘기게 되는데 이는, IEEE 802.3/802.2와 큰 연관이 있다. 많은 사람들이 IEEE 802.3 자체를 이더넷이라 아는 경우가 많은데, 실제 IEEE 802.3은 좀 더 큰 의미라고 보는 것이 맞을 듯 하다. IEEE 802.3은 EthernetII라고 불리는 DIX II (RFC 894)표준부터, RFC 1042등을 모두 포함한 표준이다. 위의 경우 RFC 1042인 IEEE 802.3 혹은, IEEE 802.2 LLC 프레임을 포함한 IEEE 802.3를 if문을 통해 EthernetII와 구분하게 된다. 이때, h_proto의 위치에 패킷의 길이를 나타내는 len이 들어가는 이유는 아래 IEEE 802.3 Frame의 그림을 보자



위와 같이 기존 EthernetII에서 EtherType에 해당하는 부분이 Length로 대체된 것을 알 수 있다. 커널 코드에서는 이를 반영해 EthernetII가 아닌, IEEE 802.3 Frame기반의 프로토콜은 해당 명세에 맞게 h_proto에 len을 저장하게 된다.

이제 남은 부분을 모두 정리하도록 하겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    if (!saddr)
        saddr = dev->dev_addr;
    memcpy(eth->h_source, saddr, ETH_ALEN);
 
    if (daddr) {
        memcpy(eth->h_dest, daddr, ETH_ALEN);
        return ETH_HLEN;
    }
 
    if (dev->flags & (IFF_LOOPBACK | IFF_NOARP)) {
        eth_zero_addr(eth->h_dest);
        return ETH_HLEN;
    }
 
    return -ETH_HLEN;
cs

[ eth.c - eth_header(...) ]


saddr에 별도로 정의된 값이 없을 경우, 네트워크 디바이스 구조체의 dev_addr멤버에 명시되어 있는 H/W Address를 읽어 memcpy로서 복사하게 된다. net_device는 NIC 마다 커널에 로드되는 네트워크 디바이스 드라이버 고유 구조체로서, 각 NIC마다 독립적인 구조체를 가지고 있다. 이 때 해당 net_device구조체의 dev_addr은 해당 NIC의 MAC주소를 가지게 되며, 따라서 위 코드는 네트워크 통신을 수행할 디바이스에서 직접적으로 MAC주소를 수집해 넣는 것으로 보아도 무방하다.

다음으로, daddr. daddr은 일반적인 경우 특정 값이 전달되기 때문에 그대로 memcpy를 통해 h_source에 복사하면 된다. 이 경우가 위 코드의 5번 라인에서 8번 라인에 해당하는 과정이다. 그러나 일부 특수한 경우 daddr의 값이 정해지지 않은 채로 넘어오는 경우가 있는데(null ptr), saddr의 경우 송신자가 본인인 것을 알 수 있으니까 디바이스에서 주소를 얻어올 수 있다지만 Destination이 정의되지 않은 경우에는 어쩔 수 없이 15번 라인과 같이 -ETH_HLEN이라는 에러코드를 리턴하게 된다.

하지만, 이에도 예외가 있는데 10번 라인에서 12번 라인까지를 살펴보자.


IFF_NOARP(ARP를 수행하지 않는 인터페이스, Destination Address를 설정하지 않음)혹은 IFF_LOOPBACK(루프백 인터페이스)일 경우에는 daddr이 설정되어 있지 않더라도 eth_zero_addr함수를 이용해 memset으로 0x00을 채워주게 된다.


본 함수의 경우 일반적인 루틴을 수행하고 리턴을 한 경우, 7번 라인에서 ETH_HLEN, 즉 이더넷 헤더 길이를 리턴하게 되고, 그 이외의 경우에는 -ETH_HLEN 에러코드를 리턴하여 헤더 할당 도중에 문제가 생겼음을 알린다.

최종적으로 정리하자면, 네트워크 통신을 수행할 sk_buff를 받아서 이더넷 헤더를 추가 및 값을 설정 해주는 함수라고 보면 될 듯 하다.

Posted by RevDev
,