본문 바로가기

리눅스 커널/네트워크

ioctl - socket

ioctl 호출 시 file descriptor, command, args 가 필요한데,

 

일반적인 eth0 장치에 대해서는 세번째 인자로 아래의 자료구조를 사용하며,

struct ifreq {
#define IFHWADDRLEN	6
	union
	{
		char	ifrn_name[IFNAMSIZ];		/* if name, e.g. "en0" */
	} ifr_ifrn;
	
	union {
		struct	sockaddr ifru_addr;
		struct	sockaddr ifru_dstaddr;
		struct	sockaddr ifru_broadaddr;
		struct	sockaddr ifru_netmask;
		struct  sockaddr ifru_hwaddr;
		short	ifru_flags;
		int	ifru_ivalue;
		int	ifru_mtu;
		struct  ifmap ifru_map;
		char	ifru_slave[IFNAMSIZ];	/* Just fits the size */
		char	ifru_newname[IFNAMSIZ];
		void __user *	ifru_data;
		struct	if_settings ifru_settings;
	} ifr_ifru;
};
#endif /* __UAPI_DEF_IF_IFREQ */

#define ifr_name	ifr_ifrn.ifrn_name	/* interface name 	*/
#define ifr_hwaddr	ifr_ifru.ifru_hwaddr	/* MAC address 		*/
#define	ifr_addr	ifr_ifru.ifru_addr	/* address		*/
#define	ifr_dstaddr	ifr_ifru.ifru_dstaddr	/* other end of p-p lnk	*/
#define	ifr_broadaddr	ifr_ifru.ifru_broadaddr	/* broadcast address	*/
#define	ifr_netmask	ifr_ifru.ifru_netmask	/* interface net mask	*/
#define	ifr_flags	ifr_ifru.ifru_flags	/* flags		*/
#define	ifr_metric	ifr_ifru.ifru_ivalue	/* metric		*/
#define	ifr_mtu		ifr_ifru.ifru_mtu	/* mtu			*/
#define ifr_map		ifr_ifru.ifru_map	/* device map		*/
#define ifr_slave	ifr_ifru.ifru_slave	/* slave device		*/
#define	ifr_data	ifr_ifru.ifru_data	/* for use by interface	*/
#define ifr_ifindex	ifr_ifru.ifru_ivalue	/* interface index	*/
#define ifr_bandwidth	ifr_ifru.ifru_ivalue    /* link bandwidth	*/
#define ifr_qlen	ifr_ifru.ifru_ivalue	/* Queue length 	*/
#define ifr_newname	ifr_ifru.ifru_newname	/* New name		*/
#define ifr_settings	ifr_ifru.ifru_settings	/* Device/proto settings*/

추가로 무선에 대한 wlan0 에 대해서는 아래의 자료구조를 사용한다.

struct iwreq {
	union
	{
		char	ifrn_name[IFNAMSIZ];	/* if name, e.g. "eth0" */
	} ifr_ifrn;

	/* Data part (defined just above) */
	union iwreq_data	u;
};

 

command는 아래의 매크로 값을 사용하는데, 

#define SIOCGIFNAME	0x8910		/* get iface name		*/
#define SIOCSIFLINK	0x8911		/* set iface channel		*/
#define SIOCGIFCONF	0x8912		/* get iface list		*/
#define SIOCGIFFLAGS	0x8913		/* get flags			*/
#define SIOCSIFFLAGS	0x8914		/* set flags			*/
#define SIOCGIFADDR	0x8915		/* get PA address		*/
#define SIOCSIFADDR	0x8916		/* set PA address		*/
#define SIOCGIFDSTADDR	0x8917		/* get remote PA address	*/
#define SIOCSIFDSTADDR	0x8918		/* set remote PA address	*/
#define SIOCGIFBRDADDR	0x8919		/* get broadcast PA address	*/
#define SIOCSIFBRDADDR	0x891a		/* set broadcast PA address	*/
#define SIOCGIFNETMASK	0x891b		/* get network PA mask		*/
#define SIOCSIFNETMASK	0x891c		/* set network PA mask		*/

 

매크로는 이름의 이해는 아래와 같이 해석 하면 편하다.

S는 Socket, IO는 Input/Ouput, C는 Configuration, G는 Get, S는 Set

IF는 InterFace 정도로 해석하면 된다.

 

아래는 ioctl 예제이다.

#include <stdio.h>
#include <string.h>
#include <sys/socket.h> 
#include <arpa/inet.h> 
#include <net/if.h> //if_nameindex(), if_freenameindex()
#include <netinet/ether.h> 
#include <sys/ioctl.h>
#include <unistd.h>

int main(void)
{
	struct if_nameindex * if_arr, * itf;
	struct ifreq ifr;	
	int sock;

	sock = socket(PF_INET, SOCK_STREAM, 0); //dont care, TCP, UDP
	if (sock < 0)
	{
		perror("socket error");
		return -1;
	}

	if_arr = if_nameindex();
	if (!if_arr)
	{
		perror("if_nameindex error");
		return -1;
	}

	for (itf = if_arr; itf->if_index != 0 || itf->if_name != NULL; itf++)
	{	
		memcpy(ifr.ifr_name, itf->if_name, sizeof(ifr.ifr_name)); //necessary "ref dev_get_by_name()"
		printf("%d, %s\n", itf->if_index, itf->if_name);

		ioctl(sock, SIOCGIFADDR, &ifr);
		printf("%s\n", inet_ntoa(*(struct in_addr *)&ifr.ifr_addr.sa_data[2]));

		ioctl(sock, SIOCGIFHWADDR, &ifr);	
		printf("%s\n", ether_ntoa((struct ether_addr *)ifr.ifr_hwaddr.sa_data));
	}

	if_freenameindex(if_arr);
	close(sock);
	return 0;
}

 

if_nameindex()는 man page를 참조하길 바란다.

 

ioctl 예제 소스에서 언급할 부분은 크게 두 가지로,

 

첫번째는 소켓을 대상으로 한 ioctl을 사용하기 전에 ifreq 구조체 내 인터페이스 이름을 먼저 설정해 주어야 한다.

net_device 예제에서 소켓에 대한 내부적인 ioctl이 dev_get_by_name()으로 인터페이스 이름을 전달한 것과 같은 맥락이다.

 

두번째는 주소의 출력에 대한 점 이다.

IP주소는 4Bytes의 big-endian(Network Byte Order)으로 표현되어, 문자열 형태로 출력시에는 inet_ntoa() 쓰이는데,

이 함수는 입력으로 in_addr 구조체를 요구한다. 그래서 이에 맞게 형변환을 시켜 주는데 한 가지 문제가 있다.

struct sockaddr {
	sa_family_t	sa_family;	/* address family, AF_xxx	*/
	char		sa_data[14];	/* 14 bytes of protocol address	*/
};

sockaddr 구조체는 위와 같으며, sa_data는 원래 Port 번호와 IP주소를 같이 묶어 저장 하는데 사용이 되며,

이 구조체를 사용하기 편하게 만든게 아래 구조체이다.

struct sockaddr_in
{
    __SOCKADDR_COMMON (sin_);
    in_port_t sin_port;                 /* Port number.  */
    struct in_addr sin_addr;            /* Internet address.  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
                           __SOCKADDR_COMMON_SIZE -
                           sizeof (in_port_t) -
                           sizeof (struct in_addr)];
};

맨 위는 매크로를 따라가면 sin_family라는 이름의 변수이고, 

sin_port는 2Bytes로 표현되는 포트 번호를 담는다.

sin_addr는 4Bytes로 표현되는 IPv4 주소를 담고,

마지막 sin_zero는 사용되지 않는다.

 

그래서 이렇게 3가지가 총 14Bytes의 sa_data를 표현하게 되므로,

sa_data[2] 부터는 IPv4 주소를 저장한다고 볼 수 있는 것 이다.

 

ether_ntoa()는 6바이트의 이더넷 주소를 사람이 이해하기 편한 형태로 변환 시켜주는 함수로,

ether_addr 이라는 구조체를 입력으로 받는데 이 구조체는 6Bytes의 배열을 래핑시킨 것이기 때문에 sa_data 전체를 넘기면 된다.

 

'리눅스 커널 > 네트워크' 카테고리의 다른 글

Raw socket incoming packet capture  (0) 2021.10.24
Ethernet & ARP Header  (0) 2021.10.24
IP & ICMP Header  (0) 2021.10.24
TCP & UDP Header  (0) 2021.10.24
struct net_device example  (0) 2021.10.24