首先,我要通过编程直接获取,而不是去读诸如ifconfig
等命令的输出。
其实是只想获取IPv6地址的,不过我猜想它们差不多,也确实看到不少相关搜索结果,于是顺带着看了。
首先,使用gethostbyname
查自己通常是不行的,因为可能得到127.0.0.1
,而且我猜,这样不能处理拥有多个IPv4地址的情况。另外一种方式是连上某个主机,然后调用getsockname
。这样需要能够直接连上那个主机,好处是如果有多个网络接口,这样可以知道到底走的是哪个接口,调试网络时不错。我最满意的方案在这里,使用ioctl
来获取。这个方法可以获取指定网络接口的IPv4地址。至于有哪些网络接口嘛,直接读/proc/net/dev
吧。
import fcntl
import socket
import struct
ifname = b'eth0'
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 0x8915 是 SIOCGIFADDR
ip = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, struct.pack('256s', ifname[:15]))[20:24])
print(ip)
然而,这样只能获取IPv4地址。创建个AF_INET6
的 socket 传过去会报错「Inappropriate ioctl for device」。那怎么办呢?Google 没找到,我去搜了下内核源码。inet_ioctl
里有对SIOCGIFADDR
的处理。但是,inet6_ioctl
里却没有了。
于是,我只好去下载ifconfig
所属的 net-tools 的源码,找到相关代码:
#if HAVE_AFINET6
/* FIXME: should be integrated into interface.c. */
if ((f = fopen(_PATH_PROCNET_IFINET6, "r")) != NULL) {
while (fscanf(f, "%4s%4s%4s%4s%4s%4s%4s%4s %08x %02x %02x %02x %20s\n",
addr6p[0], addr6p[1], addr6p[2], addr6p[3],
addr6p[4], addr6p[5], addr6p[6], addr6p[7],
&if_idx, &plen, &scope, &dad_status, devname) != EOF) {
if (!strcmp(devname, ptr->name)) {
sprintf(addr6, "%s:%s:%s:%s:%s:%s:%s:%s",
addr6p[0], addr6p[1], addr6p[2], addr6p[3],
addr6p[4], addr6p[5], addr6p[6], addr6p[7]);
这里就是ifconfig
输出IPv6部分的代码了。可以看到它打开了一个奇怪的文件。跟过去,发现是
#define _PATH_PROCNET_IFINET6 "/proc/net/if_inet6"
囧,这个文件我早就发现过了的。看来和IPv4的情况不同,IPv6地址只能通过/proc
里的文件获取了。而且输出成人可读格式不容易(ifconfig
是自己实现的)。
PS: 我还发现了件好玩的事,在 Linux 源码的include/linux/sockios.h
中,SIOCGIFINDEX
中的字母 C 写漏了。通过git blame
我发现,这个拼写错误在至少七年前 Linux 内核代码迁移到 git 前就修正了。Linus Torvalds 说之前的代码导入到 git 后有 3.2GB。我不得不承认这是个无比正确的决定,因为现在的.git
已经有600多兆了,git 不支持断点续传,clone 下来已经很不容易了。
另外,我还联想到了 Unix 系统调用中的creat
,以及 HTTP 协议中的referer
:D
#define SIOCGIFINDEX 0x8933 /* name -> if_index mapping */
#define SIOGIFINDEX SIOCGIFINDEX /* misprint compatibility :-) */