博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

详解 Python 获取网卡 IP 地址

Posted on 2018-06-15 15:57  bw_0927  阅读(8652)  评论(0编辑  收藏  举报

https://bitmingw.com/2018/05/06/get-ip-address-of-network-interface-in-python/

 

在 StackOverflow 上流传着这样一份用 Python 获取网卡 IPv4 地址的神秘代码。

import socket
import fcntl
import struct

def get_ip_address(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915,
struct.pack('256s', ifname[:15]))[20:24])

get_ip_address('eth0')

 

网卡名字可以从/sys/class/net或者/proc/net/dev中找到。


但是,很少有人知道这段代码是如何工作的。本文将为你揭开这段代码的神秘面纱。

Python socket

Python 的 socket 模块提供了有关网络接口的底层控制方法。socket.socket 函数会创建一个新的 socket 对象。它的用法如下:

1
socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

第一个参数 family 指定了网络地址的类型。最常用的值有两个:默认值 AF_INET 对应 IPv4,AF_INET6 是 IPv6。

第二个参数 type 代表了传输层协议的类型。默认值 SOCK_STREAM 是我们熟知的 TCP 协议,而 SOCK_DGRAM 则对应 UDP 协议。

因此,s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 的含义是,创建一个使用 IPv4 网络和 UDP 协议的 socket 对象 s

为了获取网卡的 IP 地址,创建 TCP socket 或是 UDP socket 是没有差别的。由于 socket.AF_INET 和 socket.SOCK_STREAM 都是 socket.socket 函数的默认参数,所以这一行实际上可以简写成

1
s = socket.socket()

socket 对象 s 创建后,可以通过 s.fileno() 获取这个 socket 的文件描述符(file descriptor)。

另外,代码中的 socket.inet_ntoa 函数把一个 4 字节 IP 地址(即 struct in_addr)转化成点分十进制的可读形式。

现在,我们可以推断出,从 fcntl.ioctl 到 [20:24] 这一大段内容,是用来获取网卡对应的 4 字节 IP 地址的。

 

fcntl 与 ioctl

fcntl 与 ioctl 是 UNIX/Linux 系统中用于文件控制和 I/O 控制的两个系统调用。Python 在此基础上进行了封装。函数 ioctl 的用法如下:

fcntl.ioctl(fd, request, arg=0, mutate_flag=True)

参数 fd 是我们想控制的文件的文件描述符。在 UNIX/Linux 系统中,I/O 设备也用文件来表示,因此这里需要传入 socket 的文件操作符。

第二个参数 request 是我们想进行的操作。这个操作由一个预定义的 32 位整数表示。代码中使用的 0x8915 在 /usr/include/linux/sockios.h 文件中定义,它对应的符号是 SIOCGIFADDR,我们正是通过这一操作来取得 IPv4 地址。想要查询 ioctl 支持的所有操作,可以在命令行中输入 man ioctl_list

第三个参数 arg 是操作所需的参数,这通常是一个 32 位整数或一段二进制内容。根据文档(man netdevice),使用 SIOCGIFADDR 时需要传入的参数是结构体 struct ifreqstruct ifreq 的定义位于 /usr/include/net/if.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ifreq {
char ifr_name[IFNAMSIZ]; /* Interface name */
union {
struct sockaddr ifr_addr;
struct sockaddr ifr_dstaddr;
struct sockaddr ifr_broadaddr;
struct sockaddr ifr_netmask;
struct sockaddr ifr_hwaddr;
short ifr_flags;
int ifr_ifindex;
int ifr_metric;
int ifr_mtu;
struct ifmap ifr_map;
char ifr_slave[IFNAMSIZ];
char ifr_newname[IFNAMSIZ];
char *ifr_data;
};
};

在我的计算机上,IFNAMSIZ 的值是 16,即网卡名称最长为 15 字节(第16个字节必须是 \0 用来表示字符串的结尾),而 struct ifreq 的大小是 40 字节。对于 SIOCGIFADDR 来说,只有前 16 个字节 ifr_name 是有意义的,后面的值都可以设定为 0x00

操作 SIOCGIFADDR 返回的结果也是 struct ifreq 结构体。其中,网卡的 IPv4 地址信息包含在 struct sockaddr ifr_addr 结构体内。这个 4 字节的 IP 地址位于 struct ifreq 结构体 20-23 字节处。所以我们会看到,fcntl.ioctl 返回的结果后面有 [20:24] ——只需要把这 4 个字节拿去转换就可以了。

到现在为止,如果我们可以正确生成 40 字节的 struct ifreq 结构体,就可以通过 ioctl 拿到 IPv4 的地址。生成结构体需要用到 Python 的 struct 模块。

 

Python struct 与 unicode string

Python 的 struct 模块用于生成和解析二进制内容。struct.pack 的用法如下:

1
struct.pack(fmt, v1, v2, ...)

这个函数比较像 printf,第一个参数用于设定格式,后续的参数用于填充内容。

struct.pack('256s', ifname[:15]) 用 ifname 的前 15 个字节填充了一个 256 字节的二进制空间,未指定内容的空间会用字节 0x00 填充。事实上,由于 struct ifreq 的大小只有 40 字节,将 256s 换成 40s 也能得到期望的 struct ifreq 结构体。

最后我们来讲一讲字符串的问题。Python 2 是不区分 str 和 bytes 的,所以 ifname这个字符串可以直接拿来当一组字节用。代码中的 ifname[:15] 是一种防御性的措施,即只保留前 15 个字节。如果确信用户的输入合法,直接使用 ifname 也可以。但是在 Python 3 中,由于字符串不能隐式地当作一组字节用,所以需要额外的转换,具体来说就是把

1
2
3
# Python 2

struct.pack('256s', ifname[:15])

变成

1
2
3
# Python 3

struct.pack('256s', bytes(ifname[:15], 'utf-8'))

其中 utf-8 是字符串 ifname 的编码方法。

 

自学之道

看到这里,你可能回想,如果今后遇到类似的代码,应该如何分析它背后的原理呢?技法不外乎两条,一是多看文档,二是多读源码。

本文例子中使用的 Python 函数,在官方文档中有详细的表述。细心阅读之后,不难搞清使用每个函数的意图,并进一步推断该函数的参数需满足的条件,以及返回值的形态。

当涉及到 ioctl 等 UNIX/Linux 系统调用的时候,仅仅依靠阅读文档(而且这种文档可能不太容易找到)是不能完全掌握代码意图的。涉及到具体逻辑,参数与返回值,结构体内容与大小等细节问题时,就需要研读源码,探寻蛛丝马迹。研读源码时,要有的放矢,先用搜索缩小范围,再逐行精读。有时,甚至需要通过动手实验(例如用 sizeof查看结构体大小)来尝试发现新的线索。

看文档与读源码,都是需要很多耐心的工作。剖析代码可能动辄需要几个小时的时间,但当真相大白之时,相信你能够感受到那豁然开朗的快乐。

 

 


 

 

struct sockaddr和struct sockaddr_in这两个结构体用来处理网络通信的地址。

在各种系统调用或者函数中,只要和网络地址打交道,就得用到这两个结构体。

网络中的地址包含3个方面的属性:

1 地址类型: ipv4还是ipv6

2 ip地址

3 端口

相应的,头文件有如下定义:

 

C代码  收藏代码
  1. include <netinet/in.h>  
  2.   
  3. struct sockaddr {  
  4.     unsigned short    sa_family;    // 2 bytes address family, AF_xxx  
  5.     char              sa_data[14];     // 14 bytes of protocol address  
  6. };  
  7.   
  8. // IPv4 AF_INET sockets:  
  9.   
  10. struct sockaddr_in {  
  11.     short            sin_family;       // 2 bytes e.g. AF_INET, AF_INET6  
  12.     unsigned short   sin_port;    // 2 bytes e.g. htons(3490)  
  13.     struct in_addr   sin_addr;     // 4 bytes see struct in_addr, below  
  14.     char             sin_zero[8];     // 8 bytes zero this if you want to  
  15. };  
  16.   
  17. struct in_addr {  
  18.     unsigned long s_addr;          // 4 bytes load with inet_pton()  
  19. };  

 

 

注释中标明了属性的含义及其字节大小,这两个结构体一样大,都是16个字节,而且都有family属性,不同的是:

sockaddr用其余14个字节来表示sa_data,而sockaddr_in把14个字节拆分成sin_port, sin_addr和sin_zero

分别表示端口、ip地址。sin_zero用来填充字节使sockaddr_in和sockaddr保持一样大小。

 

 

sockaddr和sockaddr_in包含的数据都是一样的,但他们在使用上有区别:

程序员不应操作sockaddr,sockaddr是给操作系统用的

程序员应使用sockaddr_in来表示地址,sockaddr_in区分了地址和端口,使用更方便。

 

 

一般的用法为:

程序员把类型、ip地址、端口填充sockaddr_in结构体,然后强制转换成sockaddr,作为参数传递给系统调用函数

网络编程中一段典型的代码为:

 

C代码  收藏代码
  1. int sockfd;  
  2. struct sockaddr_in servaddr;  
  3.   
  4. sockfd = Socket(AF_INET, SOCK_STREAM, 0);  
  5.   
  6. /* 填充struct sockaddr_in */  
  7. bzero(&servaddr, sizeof(servaddr));  
  8. servaddr.sin_family = AF_INET;  
  9. servaddr.sin_port = htons(SERV_PORT);  
  10. inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);  
  11.   
  12. /* 强制转换成struct sockaddr */  
  13. connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));  
  14.