ppp-2.4.9 源码分析
文章目录
PPP协议工作流程
当用户拨号接入ISP后,就建立了一条从用户个人电脑到ISP的物理连接。这时,用户个人电脑向ISP发送一系列的链路控制协议LCP分组(封装成多个PPP帧),以便建立LCP连接。这些分组及其响应选择了将要使用的一些PPP参数。接着还要进行网络层配置,网络控制协议NCP给新接入的用户个人电脑分配一个临时的IP地址。这样,用户个人电脑就成为互联网上的一个有IP地址的主机了。
当用户通信完毕时,NCP释放网络层连接,收回原来分配出去的IP地址。接着,LCP释放数据链路层连接。最后释放的是物理层的连接。
PPP链路的起始和终止状态永远是“链路静止”(Link Dead)状态,这时在用户个人电脑和ISP的路由器之间并不存在物理层的连接。
当用户个人电脑通过调制解调器呼叫路由器时(通常是在屏幕上用鼠标点击一个连接按钮),路由器就能够检测到调制解调器发出的载波信号。在双方建立了物理层连接后,PPP就进入“链路建立”(Link Establish)状态,其目的是建立链路层的LCP连接。
这时LCP开始协商一些配置选项,即发送LCP的配置请求帧(Configure-Request)。这是个PPP帧,其协议字段置为LCP对应的代码,而信息字段包含特定的配置请求。链路的另一端可以发送以下几种响应中的一种:
- 配置确认帧(Configure-Ack) 所有选项都接受。
- 配置否认帧(Configure-Nak) 所有选项都理解但不能接受。
- 配置拒绝帧(Configure-Reject) 选项有的无法识别或不能接受,需要协商。
LCP配置选项包括链路上的最大帧长、所使用的鉴别协议(authentication protocol)的规约(如果有的话),以及不使用PPP帧中的地址和控制字段(因为这两个字段的值是固定的,没有任何信息量,可以在PPP帧的首部中省略这两个字节)。
协商结束后双方就建立了LCP 链路,接着就进入“鉴别”(Authenticate)状态。在这一状态,只允许传送LCP协议的分组、鉴别协议的分组以及监测链路质量的分组。若使用口令鉴别协议PAP (Password Authentication Protocol), 则需要发起通信的一方发送身份标识符和口令。系统可允许用户重试若干次。如果需要有更好的安全性,则可使用更加复杂的口令握手鉴别协议CHAP (Challenge-Handshake Authentication Protocol)。 若鉴别身份失败,则转到“链路终止”(Link Terminate)状态。 若鉴别成功,则进入“网络层协议”(Network-Layer Protocol)状态。
在“网络层协议”状态,PPP链路的两端的网络控制协议NCP根据网络层的不同协议互相交换网络层特定的网络控制分组。这个步骤是很重要的,因为现在的路由器都能够同时支持多种网络层协议。总之,PPP协议两端的网络层可以运行不同的网络层协议,但仍然可使用同一个PPP协议进行通信。
如果在PP链路上运行的是IP协议,则对PPP链路的每一端配置IP协议模块(如分配IP地址)时就要使用NCP中支持IP的协议——IP 控制协议IPCP (IP Control Protocol)。IPCP分组也封装成PPP帧(其中的协议字段为0x8021)在PPP链路上传送。在低速链路_上运行时,双方还可以协商使用压缩的TCP和IP首部,以减少在链路上发送的比特数。
当网络层配置完毕后,链路就进入可进行数据通信的“链路打开”(Link Open)状态。链路的两个PPP端点可以彼此向对方发送分组。两个PPP端点还可发送回送请求LCP分组(Echo-Request)和回送回答LCP分组(Echo-Reply),以检查链路的状态。
数据传输结束后,可以由链路的一端 发出终止请求LCP分组(Terminate-Request)请求终止链路连接,在收到对方发来的终止确认LCP分组(Terminate-Ack)后,转到“链路终止”状态。如果链路出现故障,也会从“链路打开”状态转到“链路终止”状态。当调制解调器的载波停止后,则回到“链路静止”状态。
上述过程可用状态图来描述。
从设备之间无链路开始,到先建立物理链路,再建立链路控制协议LCP链路。经过鉴别后再建立网络控制协议NCP链路,然后才能交换数据。由此可见,PPP协议已不是纯粹的数据链路层的协议,它还包含了物理层和网络层的内容。
ppp-2.4.9 源码分析
全局变量和结构体说明
PPP协议里包括各种链路控制协议如LCP,PAP,CHAP,IPCP等,这些控制协议都有很多共同的地方,因此PPPD将每个控制协议都用结构protent表示,并放在控制协议数组protocols[]中,一般常用的是LCP,PAP,CHAP,IPCP这四个协议。
pppd/main.c
/*
* PPP Data Link Layer "protocol" table.
* One entry per supported protocol.
* The last entry must be NULL.
*/
// PPP数据链路层“协议”表。每个支持的协议一个条目。最后一个条目必须为空。
struct protent *protocols[] = {
&lcp_protent, // LCP协议
&pap_protent, // PAP协议
&chap_protent, // CHAP协议
#ifdef CBCP_SUPPORT
&cbcp_protent,
#endif
&ipcp_protent, // IPCP协议,IPv4
#ifdef INET6
&ipv6cp_protent, // IPCP协议,IPv6
#endif
&ccp_protent,
&ecp_protent,
#ifdef IPX_CHANGE
&ipxcp_protent,
#endif
#ifdef AT_CHANGE
&atcp_protent,
#endif
&eap_protent,
NULL
};
每个控制协议由protent结构来表示,此结构包含每个协议处理用到的函数指针。
/*
* The following struct gives the addresses of procedures to call
* for a particular protocol.
*/
struct protent {
u_short protocol; /* PPP protocol number */
/* Initialization procedure */
void (*init)(int unit); // 初始化指针,在main()中被调用
/* Process a received packet */
void (*input)(int unit, u_char *pkt, int len); // 接收报文处理
/* Process a received protocol-reject */
void (*protrej)(int unit); // 协议错误处理
/* Lower layer has come up */
void (*lowerup)(int unit); // 当下层协议UP起来后的处理
/* Lower layer has gone down */
void (*lowerdown)(int unit); // 当下层协议DOWN后的处理
/* Open the protocol */
void (*open)(int unit); // 打开协议
/* Close the protocol */
void (*close)(int unit, char *reason); // 关闭协议
/* Print a packet in readable form */ //打印报文信息,调试用
int (*printpkt)(u_char *pkt, int len, printer_func printer, void *arg);
/* Process a received data packet */
void (*datainput)(int unit, u_char *pkt, int len); //处理已收到的数据包
bool enabled_flag; /* 0 iff protocol is disabled */
char *name; /* Text name of protocol */
char *data_name; /* Text name of corresponding data protocol */
option_t *options; /* List of command-line options */
/* Check requested options, assign defaults */
void (*check_options)(void); // 检测和此协议有关的选项参数
/* Configure interface for demand-dial */
int (*demand_conf)(int unit); // 将接口配置为按需拨号需要做的 动作
/* Say whether to bring up link for this pkt */
int (*active_pkt)(u_char *pkt, int len); // 判断报文类型并激活链路
};
对pppd状态机的定义
/*
* Values for phase.
*/
#define PHASE_DEAD 0 // 静止
#define PHASE_INITIALIZE 1 // 初始化
#define PHASE_SERIALCONN 2
#define PHASE_DORMANT 3 // 休眠
#define PHASE_ESTABLISH 4 // 建立
#define PHASE_AUTHENTICATE 5 // 身份认证
#define PHASE_CALLBACK 6 // 回调
#define PHASE_NETWORK 7 // 连接
#define PHASE_RUNNING 8 // 运行
#define PHASE_TERMINATE 9 // 终止
#define PHASE_DISCONNECT 10 // 断开连接
#define PHASE_HOLDOFF 11 // 延迟
#define PHASE_MASTER 12 // 控制
pppd的main函数程序开始分析其工作流程;
第一阶段 初始化
pppd/main.c
int
main(int argc, char *argv[])
{
int i, t;
char *p;
struct passwd *pw;
struct protent *protp;
char numbuf[16];
strlcpy(path_ipup, _PATH_IPUP, sizeof(path_ipup));
strlcpy(path_ipdown, _PATH_IPDOWN, sizeof(path_ipdown));
link_stats_valid = 0;
new_phase(PHASE_INITIALIZE);
// PPPD中的状态机,初始化阶段
script_env = NULL;
/* Initialize syslog facilities */
reopen_log();
if (gethostname(hostname, MAXNAMELEN) < 0 ) {
option_error("Couldn't get hostname: %m");
exit(1);
}
hostname[MAXNAMELEN-1] = 0;
/* make sure we don't create world or group writable files. */
umask(umask(0777) | 022);
uid = getuid();
privileged = uid == 0;
slprintf(numbuf, sizeof(numbuf), "%d", uid);
script_setenv("ORIG_UID", numbuf, 0);
ngroups = getgroups(NGROUPS_MAX, groups);
/*
* Initialize magic number generator now so that protocols may
* use magic numbers in initialization.
现在初始化魔术数字生成器,以便协议可以在初始化中使用魔术数字。
*/
magic_init();
/*
* Initialize each protocol. 初始化每种协议
*/
for (i = 0; (protp = protocols[i]) != NULL; ++i)
(*protp->init)(0);
// protocols[]是全局变量的协议数组,初始化协议数组中所有协议
/*
* Initialize the default channel.
*/
tty_init();
//channel初始化,默认就是全局的tty_channel,里面包括很多TTY函数指针
progname = *argv;
/*
* Parse, in order, the system options file, the user's options file,
* and the command line arguments.
*/
//按顺序解析系统选项文件、用户选项文件和命令行参数。
if (!options_from_file(_PATH_SYSOPTIONS, !privileged, 0, 1) // 解析/etc/ppp/options中的参数
|| !options_from_user()
|| !parse_args(argc-1, argv+1)) // 解析PPPD命令行参数
exit(EXIT_OPTION_ERROR);
devnam_fixed = 1; /* can no longer change device name */
/*
* Work out the device name, if it hasn't already been specified,
* and parse the tty's options file.
*/
//计算出设备名(如果它还没有被指定的话),并解析tty的选项文件。
if (the_channel->process_extra_options)
(*the_channel->process_extra_options)();
// 实际上是调用tty_process_extra_options解析TTY 参数
if (debug)
setlogmask(LOG_UPTO(LOG_DEBUG));
/*
* Check that we are running as root.
*/
// geteuid获取当前运行用户ruid,检查是否作为根用户运行。
// 本程序必须以root身份运行,否则会报错
if (geteuid() != 0) {
option_error("must be root to run %s, since it is not setuid-root",
argv[0]);
exit(EXIT_NOT_ROOT);
}
// 检测/dev/ppp设备文件是否有效
if (!ppp_available()) {
option_error("%s", no_ppp_msg);
exit(EXIT_NO_KERNEL_SUPPORT);
}
在main()函数中会调用所有支持的控制协议的初始化函数init(),之后初始化TTY channel,解析配置文件或命令行参数,接着检测内核是否支持PPP驱动。
函数ppp_available会尝试打开/dev/ppp设备文件来判断PPP驱动是否已加载在内核中。如果此设备文件不能打开则通过uname判断内核版本号来区分当前内核版本是否支持PPP驱动,要是内核版本很老(2.3.x以下),则打开PTY设备文件并设置PPP线路规程。
pppd/sys_linux.c
main() -> ppp_avaiable():
/********************************************************************
*
* ppp_available - check whether the system has any ppp interfaces
* (in fact we check whether we can do an ioctl on ppp0).
*/
// ppp_available—检查系统是否有ppp接口(事实上我们检查是否可以在ppp0上执行ioctl)。
int ppp_available(void)
{
int s, ok, fd;
struct ifreq ifr;
int size;
int my_version, my_modification, my_patch;
int osmaj, osmin, ospatch;
/* get the kernel version now, since we are called before sys_init */
// 现在获取内核版本,因为我们在sys_init之前被调用
uname(&utsname);
osmaj = osmin = ospatch = 0;
sscanf(utsname.release, "%d.%d.%d", &osmaj, &osmin, &ospatch);
kernel_version = KVERSION(osmaj, osmin, ospatch);
fd = open("/dev/ppp", O_RDWR); // 打开 /dev/ppp
if (fd >= 0) {
new_style_driver = 1; // 支持PPPK
......
/*
* Validate the version of the driver against the version that we used.
*/
// 根据我们使用的版本验证驱动程序的版本。
decode_version(VERSION,
&my_version,
&my_modification,
&my_patch);
/* The version numbers must match */
if (driver_version != my_version) // 版本号必须匹配
ok = 0;
/* The modification levels must be legal */
if (driver_modification < 3) {
// 修改级别必须合法
if (driver_modification >= 2) {
/* we can cope with 2.2.0 and above */
driver_is_old = 1;
} else {
ok = 0;
}
}
if (!ok) {
slprintf(route_buffer, sizeof(route_buffer),
"Sorry - PPP driver version %d.%d.%d is out of date\n",
driver_version, driver_modification, driver_patch);
no_ppp_msg = route_buffer;
}
}
}
close(s);
return ok;
}
接下来会检查选项参数的合法性,这些参数包括系统参数,认证相关的参数等,还会检查每个控制协议的参数配置以及tty参数。后面是将pppd以daemon方式执行或保持在前台运行并设置一些环境变量和信号处理函数
pppd/main.c
/*
* Check that the options given are valid and consistent.
*/
// 检查给出的选项是否有效和一致。
check_options(); // 检查选项参数
if (!sys_check_options()) // 检测系统参数
exit(EXIT_OPTION_ERROR);
auth_check_options(); // 检查认证相关的参数
#ifdef HAVE_MULTILINK
mp_check_options();
#endif
for (i = 0; (protp = protocols[i]) != NULL; ++i)
if (protp->check_options != NULL) // 检查每个控制协议的参数配置
(*protp->check_options)();
if (the_channel->check_options)
(*the_channel->check_options)(); // 调用tty_check_options检测TTY参数
if (dump_options || dryrun) {
init_pr_log(NULL, LOG_INFO);
print_options(pr_log, NULL);
end_pr_log();
}
if (dryrun)
die(0);
/* Make sure fds 0, 1, 2 are open to somewhere. */
fd_devnull