PPP协议工作流程,结合ppp-2.4.9 源码分析

本文详细介绍了PPP协议的工作流程,从物理连接建立到链路终止的四个阶段,包括LCP配置、身份鉴别、网络层协议配置和数据通信。此外,还分析了ppp-2.4.9源码中的全局变量、结构体和初始化过程,重点讨论了PPP按需拨号模式的实现。最后,探讨了PPP链路建立与终止的具体步骤。
摘要由CSDN通过智能技术生成

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对应的代码,而信息字段包含特定的配置请求。链路的另一端可以发送以下几种响应中的一种:

  1. 配置确认帧(Configure-Ack) 所有选项都接受。
  2. 配置否认帧(Configure-Nak) 所有选项都理解但不能接受。
  3. 配置拒绝帧(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 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值