PPPOE源码分析

一、    服务端

源文件rp-pppoe-3.11/src/pppoe-server.c

假设我们执行的命令是pppoe-server -I br-lan -L 192.168.10.1 -R 192.168.10.5 -N 10 -F

-I指定接口名称

-L指定本地IP地址

-R指定分配给客户端的起始IP地址

-N指定允许同时存在多少个session

-F在前台运行

我们可以使用-d选项调试session创建信息:

root@OpenWrt:/# pppoe-server -I br-lan -L 192.168.10.1-R 192.168.10.5 -N 10 -d

Session 1 local 192.168.10.1 remote192.168.10.5

Session 2 local 192.168.10.1 remote192.168.10.6

Session 3 local 192.168.10.1 remote192.168.10.7

Session 4 local 192.168.10.1 remote192.168.10.8

Session 5 local 192.168.10.1 remote192.168.10.9

Session 6 local 192.168.10.1 remote192.168.10.10

Session 7 local 192.168.10.1 remote192.168.10.11

Session 8 local 192.168.10.1 remote192.168.10.12

Session 9 local 192.168.10.1 remote192.168.10.13

Session 10 local 192.168.10.1 remote192.168.10.14

从main函数开始分析

进入main函数,首先是一个while循环,通过getopt解析命令行参数,按照我们输入的命令解析完成后的结果如下:

IgnorePADIIfNoFreeSessions = 0

MaxSessionsPerMac = 0

pppd_path = “/usr/sbin/pppd”         默认在src/Makefile中指定

IncrLocalIP = 0          不为每个连接增加本地IP地址

beDaemon = 0          不在后台运行,即在前台运行

NumSessionSlots =10      同时存在的最大session个数

NumInterfaces = 1            总共一个接口

interfaces[0].name = “br-lan”           接口名称

LocalIP[0] = 192 LocalIP[1] = 168 LocalIP[2]= 10 LocalIP[3] = 1      本地IP地址192.168.10.1

RemoteIP[0] = 192 RemoteIP[1] = 168 RemoteIP[2]= 10 RemoteIP[3] = 5    分配给客户端的起始IP地址192.168.10.5

pppoptfile = “/etc/ppp/pppoe-server-options”                 默认在src/Makefile中指定

ACName = “OpenWrt”    集中访问器名称(Access concentrator name)

RandomizeSessionNumbers = 0      不随机生成会话ID

然后分配10个ClientSessionStruct结构体,用该结构体表示一个客户端会话

Sessions = calloc(NumSessionSlots,sizeof(ClientSession));

初始化每个ClientSessionStruct结构体的myip字段为LocalIP(192.168.10.1),因为IncrLocalIP=0,所以不增加本地IP

for (i=0;i<NumSessionSlots; i++) {

                   memcpy(Sessions[i].myip,LocalIP, sizeof(LocalIP));

}

下面继续初始化ClientSessionStruct结构体的一些字段:

for (i=0; i<NumSessionSlots;i++) {

         Sessions[i].pid = 0;

         Sessions[i].funcs =&DefaultSessionFunctionTable;

         Sessions[i].sess =htons(i+1+SessOffset);

         if (!addressPoolFname) {

            memcpy(Sessions[i].peerip, RemoteIP, sizeof(RemoteIP));

            incrementIPAddress(RemoteIP);

         }

}

pid:用来处理会话的子进程ID

sess:会话ID

peerip:分配给客户端的IP,这里从192.168.10.5开始一次增加1

打开所以接口(按照我们执行的命令,这里只有一个接口br-lan)

/* Open all theinterfaces */

for (i=0;i<NumInterfaces; i++) {

         interfaces[i].mtu = 0;

         interfaces[i].sock = openInterface(interfaces[i].name,Eth_PPPOE_Discovery, interfaces[i].mac, &interfaces[i].mtu);

}

这里调用openInterface打开接口interfaces[0],其中的参数如下:

interfaces[0].name= “br-aln”

Eth_PPPOE_Discovery= 0x8863      表示PPPOE协议的发现阶段在以太网帧中的协议类型号

interfaces[i].mac在openInterface函数中通过ioctl获取接口的MAC地址将保存都改变量中

interfaces[i].mtu在openInterface通过ioctl获取接口的MTU将保存到该变量中

最终openInterface将以这样的方式创建一个套接字,它将监听内核中的0x8863协议类型的数据包

fd =socket(PF_PACKET, SOCK_RAW, htons(0x8863))

然后调用bind(fd, (structsockaddr *) &sa, sizeof(sa))将套接字绑定到指定的接口上,这样该套接字就只监听br-lan上的0x8863协议的数据包了。

回到main函数,为每个接口的套接字创建一个读事件处理操作

for (i = 0;i<NumInterfaces; i++) {

         interfaces[i].eh =Event_AddHandler(event_selector,

                                                   interfaces[i].sock,

                                                   EVENT_FLAG_READABLE,

                                                   InterfaceHandler,

                                                   &interfaces[i]);

这样当接口的套接字上有数据可读时,将回调InterfaceHandler函数,该函数后面再分析。

最后进入一个for(;;)循环,开始事件监听每个接口的套接字是否有数据可读。

for(;;) {

         i = Event_HandleEvent(event_selector);

         if (i < 0) {

            fatalSys("Event_HandleEvent");

}

下面分析最核心的InterfaceHandler函数,该函数直接调用serverProcessPacket

该函数首先调用receivePacket从套接字读取一个PPPoEPacket大小的数据,然后对该数据包进行一些健康检查,比如长度、版本、类型,然后根据PPPoEPacket数据包的code字段决定执行哪个操作。

switch(packet.code){

    case CODE_PADI:

         processPADI(i, &packet, len);

         break;

    case CODE_PADR:

         processPADR(i, &packet, len);

         break;

    case CODE_PADT:

         /* Kill the child */

         processPADT(i, &packet, len);

         break;

    case CODE_SESS:

         /* Ignore SESS -- children will handlethem */

         break;

    case CODE_PADO:

    case CODE_PADS:

         /* Ignore PADO and PADS totally */

         break;

    default:

         /* Syslog an error */

         break;

}

一开始,客户端将会广播PADI包,服务端收到PADI包后就执行processPADI函数,该函数进行一些判断后,然后封装PADO包,以单播方式发回客户端。

然后客户端将向服务端发送PADR包请求服务,服务端收到PADR包后,执行processPADR函数,如果PADR包OK,该函数将创建一个子进程,同时为子进程退出事件安装处理操作childHandler,即当子进程退出时将回调childHandler函数。在子进程中将回复客户端PADS包,接着通过execv启动PPPD进程,其启动的命令行参数如下:

pty /usr/sbin/pppoe -n -I br-lan -e1:44:8a:5b:ec:49:27 -S '' :通过pty指定一个脚本用来通信,而不是一个终端设备

file /etc/ppp/pppoe-server-options :指定选项文件路径

192.168.10.1:192.168.10.5 :指定本地IP地址和远程IP地址

nodetach :在前台运行

noaccomp : 禁止压缩地址和控制字段

nopcomp : 禁止压缩协议字段

mru 1492 : 指定最大接收单元为1492字节

mtu 1492 : 指定最大传输单元为1492字节

这就进入会话阶段了,由PPPD进程处理LCP协商、认证和IPCP协商。当子进程退出时,回调childHandler将向客户端发送PADT包。

当pppoe-server收到PADT包时,将执行processPADT函数,该函数执行Sessions[i].funcs->stop,实际是PppoeStopSession函数,该函数将kill掉每个会话的子进程,同时回复客户端PADT包。

二、    客户端

Netifd根据用户设置调用/lib/netifd/proto/ppp.sh启动pppd,其传入的参数如下:

nodetach 前台运行

ipparam wan   给ip-up,ip-pre-up 和 ip-down这些脚本提供的一个额外的字符串

ifname pppoe-wan  为ppp接口设置物理名称

+ipv6         使能IPv6CP和IPv6协议

nodefaultroute         禁止defaultroute选项

usepeerdns       向对端询问DSN地址

persist      当一个连接终止后不退出,而是重新打开连接

maxfail 1  连续尝试多少次失败后终止连接

user root 设置用于对端认证的用户名

password xxx   设置用于对端认证的密码

ip-up-script /lib/netifd/ppp-up

ipv6-up-script /lib/netifd/ppp-up

ip-down-script /lib/netifd/ppp-down

ipv6-down-script /lib/netifd/ppp-down   上面3个参数指定几个脚本的执行路径

mtu 1492          指定最大传输单元为1492字节

mru 1492          指定最大接收单元1492

plugin rp-pppoe.so  指定要加载的动态库路径(插件)

nic-eth0.2         指定给pppoe的接口为eth0.2

源文件ppp-2.4.7/pppd/main.c

进入main函数,首先通过new_phase(PHASE_INITIALIZE)标识pppd现在为初始化阶段

然后初始化所有的协议

for (i = 0; (protp = protocols[i]) != NULL;++i)

(*protp->init)(0);

每个协议都实现了一个protent结构的实例,保存在全局数组protocols中。常见的协议比如LCP、PAP、CHAP、IPCP。

这里调用每个协议的init函数初始化协议。

然后初始化默认的通道

tty_init();

然后解析系统选项文件、用户选项文件和命令行参数

if (!options_from_file(_PATH_SYSOPTIONS,!privileged, 0, 1)

         ||!options_from_user()

         ||!parse_args(argc-1, argv+1))

         exit(EXIT_OPTION_ERROR);

我们指定了插件rp-pppoe.so,在解析到plugin时,会调用loadplugin,该函数将打开动态库rp-pppoe.so,然后执行其初始化函数plugin_init,该插件的源文件路径为ppp-2.4.7/pppd/plugins/rp-pppoe,plugin_init调用add_options添加了pppoe的选项到extra_options,当解析到nic-eth0.2时,会调用PPPoEDevnameHook函数,该函数会解析出接口名称eth0.2,然后把它保存到全局变量devnam,然后把全局变量the_channel初始化为pppoe_channel,接着调用PPPOEInitDevice初始化conn(一个PPPoEConnection类型的全局变量)

经过一些选项解析后,下面列出一些变量的值如下:

demand = 0

the_channel = &pppoe_channel     ppp对应的通道

modem = 0       Notuse modem control lines

然后解析通道的选项,即pppoe_channel的选项

if (the_channel->process_extra_options)

         (*the_channel->process_extra_options)();

这里实际调用PPPOEDeviceOptions函数

然后调用ppp_available检查ppp模块是否可用

if (!ppp_available()) {

         option_error("%s",no_ppp_msg);

         exit(EXIT_NO_KERNEL_SUPPORT);

}

该函数通过打开设备节点/dev/ppp的成功与否来判断ppp模块是否可用,该设备节点对应内核驱动ppp_generic.c

然后检查每个协议的选项

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)();

然后调用sys_init进行一些系统初始化,该函数创建了一个套接字

sock_fd = socket(AF_INET, SOCK_DGRAM, 0);

我们没有配置按需拨号,因此下面这段代码不会执行

if (demand) {

         /*

          * Open the loopback channel and set it up tobe the ppp interface.

          */

         fd_loop= open_ppp_loopback();

         set_ifunit(1);

         /*

          * Configure the interface and mark it up, etc.

          */

         demand_conf();

}

然后调用lcp_open打开LCP协议

lcp_open(0);

这里的参数0表示操作第1个ppp接口,实际上只有1个(全局宏NUM_PPP = 1)

该函数中首先取得ppp的状态机

fsm *f = &lcp_fsm[0];

这里的lcp_fsm[0]在lcp_init中被初始化为:

f->unit = 0;

f->protocol = PPP_LCP;

f->callbacks = &lcp_callbacks;

f->state = INITIAL;

f->flags = 0;

f->id = 0;                              /* XXX Start with random id? */

f->timeouttime = DEFTIMEOUT;

f->maxconfreqtransmits = DEFMAXCONFREQS;

f->maxtermtransmits = DEFMAXTERMREQS;

f->maxnakloops = DEFMAXNAKLOOPS;

f->term_reason_len = 0;

然后以参数&lcp_fsm[0]调用fsm_open,该函数根据fsm的state执行相应的操作,一开始其state= INITIAL,所以执行

f->state = STARTING;

if( f->callbacks->starting )

(*f->callbacks->starting)(f);

把状态切换到STARTING,然后调用fsm的starting函数,实际为lcp_starting,该函数最终以参数0调用link_required,该函数体为空,什么也没做。

回到main函数,然后以参数0调用start_link来启动链路,同样,这里的参数0表示第1个ppp接口

start_link(0)

该函数首先调用通道(pppoe_channel)的connect函数,实际上是PPPOEConnectDevice函数,该函数首先创建PPPOE会话阶段的套接字

conn->sessionSocket = socket(AF_PPPOX,SOCK_STREAM, PX_PROTO_OE)

该套接字用于会话阶段收发数据,即收发0x8864类型的以太网帧

然后创建PPPOE发现阶段的套接字

conn->discoverySocket = openInterface(conn->ifName,Eth_PPPOE_Discovery, conn->myEth);

openInterface的核心代码为fd =socket(PF_PACKET, SOCK_RAW, htons(0x8863))),该套接字用于收发0x8863类型的以太网帧

然后调用discovery函数处理发现阶段,主要代码如下:

sendPADI(conn);       // 发送PADI包

waitForPADO(conn, timeout);          //      等待PADO包

sendPADR(conn);     // 发送PADR包

waitForPADS(conn, timeout);           // 等待PADS包

当收到PADS后,更新连接状态为STATE_SESSION

conn->discoveryState = STATE_SESSION;

discovery执行完返回到PPPOEConnectDevice,该函数接着检查连接状态是否为STATE_SESSION,如果不是则返回错误

if (conn->discoveryState !=STATE_SESSION) {

error("Unableto complete PPPoE Discovery");

goto errout;

}

如果连接状态为STATE_SESSION,则继续执行,设置会话ID

ppp_session_number =ntohs(conn->session);

然后调用connect将会话套接字连接到AC(集中访问器),

connect(conn->sessionSocket, (struct sockaddr*) &sp, sizeof(struct sockaddr_pppox))

这会调用到内核的pppoe_connect函数,该函数为该套接字创建一个通道(struct channel),并调用ppp_register_channel将该通道注册进内核(添加到全局链表new_channels),并绑定会话ID。

关于AF_PPPOX套接字参考内核pppox.c、pppoe.c

然后PPPOEConnectDevice返回会话套接字,回到start_link,接着调用通道的establish_ppp,实际是generic_establish_ppp

该函数首先对会话套接字调用ioctl(fd, PPPIOCGCHAN, &chindex)获取之前connect创建的通道索引,这里调用ioctl最终会调用到内核的pppox_ioctl,该函数通过ppp_channel_index获取通道的索引返回给user。

然后对ppp驱动/dev/ppp调用ioctl(fd, PPPIOCATTCHAN, &chindex)将会话套接字的通道绑定到ppp驱动,这里调用ioctl最终将调用到内核的ppp_ioctl,一开始ppp还没绑定,所有会调用ppp_unattached_ioctl,该函数调用ppp_find_channel从全局链表new_channels找到对应索引的通道,然后把它和ppp绑定

file->private_data = &chan->file;

回到PPPOEConnectDevice,然后调用set_ppp_fd,更新全局变量ppp_fd,这个文件描述符表示已经绑定到会话套接字的通道的ppp设备文件描述符。

然后调用make_ppp_unit创建以ppp接口(用ifconfig看到的接口,比如ppp0),该函数对/dev/ppp调用

ioctl(ppp_dev_fd, PPPIOCNEWUNIT,&ifunit)来创建ppp接口,这里的ppp_dev_fd是对/dev/ppp的重新打开,因此调用到内核的ppp_ioctl时,一开始也将调用ppp_unattached_ioctl来创建ppp接口,该函数调用ppp_create_interface来创建网络设备net_device,在内核中将创建一个struct ppp结构实例,并绑定到file

file->private_data = &ppp->file

现在对/dev/ppp就要2个打开的文件描述符了ppp_fd对应于ppp通道(内核中的struct channel),ppp_dev_fd对应于ppp接口(内核中的struct ppp),然后generic_establish_ppp返回ppp_fd(对应通道)回到start_link,使用notice打印一句提示信息

notice("Connect: %s <-->%s", ifname, ppp_devnam);

实际输出为:Connect: ppp0 <--> eth0.2

然后把对应通道的ppp_fd加入到select的读文件描述符集

add_fd(fd_ppp);

然后调用lcp_lowerup(0)启动底层协议,该函数首先取得ppp状态机,然后调用fsm_lowerup(f),该函数又调用fsm_sconfreq(f, 0)发送一个初始化配置请求,该函数调用fsm_sdata(f, CONFREQ, f->reqid, outp, cilen)发送数据,该函数又调用output(f->unit,outpacket_buf, outlen + PPP_HDRLEN)

该函数取出协议字段包存到proto,现在是使用LCP协议,该值为PPP_LCP(0xc021),

然后判断ppp_dev_fd是否大于等于0而且协议是否不大于0xc000,这里ppp_dev_fd>0而且协议>0xc021,所以

fd = ppp_dev_fd不会执行,所以最终fd=ppp_fd(对于ppp通道),然后使用write(fd,p, len)将数据发送到ppp驱动

这里最终调用到内核的ppp_write,驱动根据文件类型执行相应的操作

switch (pf->kind) {

         caseINTERFACE:

                   ppp_xmit_process(PF_TO_PPP(pf));

                   break;

         caseCHANNEL:

                   ppp_channel_push(PF_TO_CHANNEL(pf));

                   break;

}

这里为CHANNEL,因此执行ppp_channel_push,该函数调用通道的start_xmit发送数据

pch->chan->ops->start_xmit(pch->chan,skb)

通道的ops在对会话套接字调用connect时,通过内核的pppoe_connect创建通道时初始化

po->chan.ops = &pppoe_chan_ops;

因此这里的start_xmit对应pppoe_xmit,该函数又调用__pppoe_xmit,该函数最终调用dev_queue_xmit从eth0.2将数据发送出去。

然后从start_link返回到main函数,开始循环检测有无数据可读,之前已经把ppp_fd(通道)添加到读监听描述符集。

当内核收到0x8864的pppoe会话帧时,将调用到pppoe_rcv->sk_receive_skb->pppoe_rcv_core->ppp_input

ppp_input进行一些判断,调用wake_up_interruptible或者调用ppp_do_recv

wake_up_interruptible唤醒用户层的ppp_fd读取数据                                            通道

ppp_do_recv->ppp_receive_frame->ppp_receive_nonmp_frame->netif_rx           接口ppp0

回到pppd 的main函数,当ppp_fd有数据可读时,调用get_input,刚才发送了一个LCP数据包,将收到一个LCP数据包。该函数依次将收到的数据包的协议与全局数组protocols中的协议比较,相等则调用对应协议的input函数,比如LCP的lcp_input,当收到IPCP的数据时,回复数据将通过ppp_dev_fd(对应接口)发送,最终调用到内核的ppp_xmit_process->ppp_send_frame,最终也调用到pch->chan->ops->start_xmit

猜你喜欢

转载自blog.csdn.net/zjhsucceed_329/article/details/80323029