一、 服务端
源文件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