bootp dhclient NetworkManager dbus dnsmasq resolvconf avahi关系厘清(持续更新)

在前一篇里面,其实只通过剖析代码确定了glibc通过resolv.conf来访问本地域名服务,resolvconf通过读取某路径下的文件把带nameserver的行写入resolv.conf两件事。接下来在继续追域名服务dnsmasq过程中就不断遇到新概念,比如题目中的6个,本文目标就是抽丝剥茧地厘清六者的关系,让自己在每个面试中都能讲这个讲半小时。

1.dhclient和bootp
    通过conf了解大概信息,proc和man查找conf位置。/var/lib/NetworkManager/dhclient-wlan0.conf,第一句话就是“由 NetworkManger 创建,合并自 /etc/dhcp/dhclient.conf”。conf内说明了要求和服务器沟通的的细节,比如需要请求 subnet-mask, broadcast-address, time-offset, routers...,需要发送host-name "liu3-CW65S"。还有一个lease文件/var/lib/NetworkManager/dhclient-1a3a1a3c-6e6a-4883-a3c5-3674000ae53e-wlan0.lease,看样子是会在读取conf后读取lease文件,文件内保存了上次会话的配置信息,上一篇文章可见的每一个request都会带上上一次获得的ip地址,想必也是从lease文件读取的信息吧。
    程序的启动参数有,-d强制前台运行,当然现在咋又是在后台了,还是看源码。dpkg -S /sbin/dhclient,apt-get source isc-dhcp-client即可。发现-d主要是no_daemon=1,quiet=0。no_daemon的唯一作用是把pid输出到文件,在我机器上是/run/sendsigs.omit.d/network-manager.dhclient-wlan0.pid。pid写入文件比如在nginx中可以用于重启或重新加载配置、刷新日志文件时找到conf对应的pid之后对进行发送相应的信号。对于dhclient来说没有pid文件-r -x这种强制关闭的命令行命名就不起效果,好了找到答案了,-d并不是man中说的要强制前台执行,而是要写pid,从而能从命令行把程序关掉。-sf /usr/lib/NetworkManager/nm-dhcp-client.action,这个选项,竟然是个可执行文件,还得看源码。
    在script_go函数内,的确是先fork再execve (scriptName, argv, envp)执行了脚本。函数是在用socket+ioctl取得接口列表并取得对应的lease后执行的。二进制可执行文件不可读,但是man是很清晰的。这个action脚本本身不适宜修改,用户自定义行为一般在conf实现,也可以用action脚本的hook。脚本有一个叫$reason的环境变量说明了当前脚本的执行动作,比如取得lease后的reason是“preinit”,对于preinit脚本会设置接口的ip为0.0.0.0,广播地址为255.255.255.255,像这种初始ip和初始广播地址是和网络接口强相关的,也许就是用一个额外的脚本实现的原因吧(还有个原因是用户定制函数用shell实现比较方便也好加载)。
    用action改了接口配置后首先进入state_init发送探测(DHCPDISCOVER)报文,make_discover内构造报文,可以细看下,因为可以顺便把bootp也给解决了。
    报文结构是一个struct dhcp_packet,完全和tcp/ip详解16-2的图一模一样。到此,bootp和dhclient的关系已经理清了,bootp是引导程序协议,dhclient基于此协议并利用特定厂商信息字段传递dhcp协议私有信息。这一点在上一篇的tcpdump输出内也得到了佐证。标识直接用的random()(种子用的时间和mac地址与),四个ip地址只设置了网关ip(重要,待进一步验证),具体其他bootp公共部分如何设置的可以看代码,基本都比较直观。dhcp部分以"\143\202\123\143"魔数打头说明是本协议,剩下的代码很长,先略过。在send_discover函数内发送探测报文,探测报文有超时重试机制。最终的发送是在send_packet里通过sendto进行的,那么writev的文件描述符在哪儿打开的呢?在dhcpv4_client_assignments函数内,先查找getservbyname有没有dhcpc的udp记录,因为/etc/services内没有,所以直接吧全局变量local_port写死为了68。之后在discover_interfaces内if_register_send内if_register_socket addr->sin_port = local_port;就帮顶了68,socket还设置了SO_REUSEADDR(用以重启,当然udp无所谓)\SO_BROADCAST(用以广播)\IP_RECVPKTINFO、SO_BINDTODEVICE。从上一篇文章tcpdump输出实例也可以得知通过68端口和dp服务器67端口通信的就是dhclient。
    发送具体逻辑在send_packet内,发送动作sendto在一个可选的while语句中,如果定义了IGNORE_HOSTUNREACH,那么对于立即返回的EHOSTUNREACH或者ECONNREFUSED错误会重试十次。然而我并不认为sendto会返回这两个错误,recvfrom差不多,并且对于udp,recvfrom取得的错误也无法确定是哪一个数据报的错误。接下来设定超时时间isc_interval_set定义找不到,反正是时隔一个间隔后重新调用发送探测报文的函数send_discover,直到已经收到回复报文(通过一个全局变量确定)或者探测足够多次之后放弃探测,并调用action脚本执行TIMEOUT命令,改命令尝试久的release信息是否可用,可用是指用旧的ip能ping通旧的路由器。
    如果没猜错isc_interval_set用的类似注册信号处理函数立即返回了,那么重复发送探测报文的算是一个线程,另外的一个线程在68端口上开了个tcp的监听。注册监听事件和超时、select等待这一块有时间可以会看下,感觉是个小型的nginx了(isc到底是个啥?)omapi_accept里有tcp新链接处理的逻辑,就不深入了。
    总之,dhclient读conf获取dhcp协议信息,读release获取上一次本机动态配置,运行action修改接口参数,再基于bootp从68端口由0.0.0.0向255.255.255.255发送udp探测报文,并在68端口建立tcp监听端口accept链接,之后大概动态分配服务器会和这个68建立长链接。尽管还有一大档子细节没弄明白,但是基本功能有个大概了。
    在没弄明白的很多问题中,dhclient是怎么启动的,dhclient是怎么更新resolve.conf的这两个得弄清楚。

2.NetworkManager
    启动:ubuntu的init启动过程见http://www.cnblogs.com/cassvin/archive/2011/12/25/ubuntu_init_analysis.html,写得清楚。那直接打开/etc/init/network-manager.conf,可见networkmanager的打开依赖local-filesystems事件(mountall命令)、started dbus、static-network-up。之后不带参数地执行/usr/sbin/NetworkManager。先readelf -s下,发现符号表竟然一屏都显示不完,说明这趟水很深。lsof -p发现一行NetworkMa 783 root    3u     unix 0xf6826080      0t0   14984 socket,类似的有三行unix域和一行FIFO。那么先从进程间通信方式下手吧。
    strace nmcli nm status &>temp,发现了两行socket(PF_LOCAL, SOCK_STREAM|SOCK_CLOEXEC, 0) = 4,connect(4, {sa_family=AF_LOCAL, sun_path="/var/run/dbus/system_bus_socket"}, 33) = 0。后面的recvmsg,sendmsg都是在这个4上进行的。看来这里已经不能避免了解dbus了。
    先看一系列博客补充知识http://blog.csdn.net/flowingflying/article/details/5410820,再看nmcli源码,和dhclient比起来还便于用gdb调。在扒之前得了解下glib的知识。https://developer.gnome.org/gobject/stable/gobject-Type-Information.html,额,貌似竟然是试图在c语言基础上实现类的一个库,比如g_string_new,怀着为什么有了c++还要glib+gobject的问题上sof,是linux图形界面gtk是纯c开发的,因为实际需求和开发接口统一发明了这一套,有的变态面试官如果出“如何用c语言实现面向对象”,可以了解下这个。言归正传,nmcli.c代码一开始是解析参数,之后直接就是bus = dbus_g_bus_get (DBUS_BUS_SYSTEM, &error)。dbus相关的基础知识http://blog.csdn.net/flowingflying/article/details/5455327可以学习到,个人感觉作为一个比tcpip简单不少的进程间的通讯协议,函数有点太多了不容易记忆。不过看起来dbus+glib是个重要知识点,本文暂停,先恶补下。

你可能感兴趣的:(学习笔记自用)