Chinaunix首页 | 论坛 | 博客
  • 博客访问: 47554
  • 博文数量: 23
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 10
  • 用 户 组: 普通用户
  • 注册时间: 2015-07-25 15:36
文章分类

全部博文(23)

文章存档

2016年(4)

2015年(19)

我的朋友

分类: 嵌入式

2015-08-21 14:17:58

原文地址:pppoe协议和pppd源码分析 作者:lwchsz

pppoe协议简介

(一)发现(Discovery)阶段
  在发现(Discovery)阶段中用户主机以广播方式寻找所连接的所有接入集中器(或交换机),并获得其以太网MAC地址。然后 选择需要连接的主机,并确定所要建立的PPP会话标识号码。发现阶段有4个步骤,当此阶段完成,通信的两端都知道PPPoESESSION-ID和对端的 以太网地址,他们一起唯一定义PPPoE会话。这4个步骤如下。
(1)主机广播发起分组(PADI)
  (1)主机广播发起分组(PADI),分组的目的地址为以太网的广播地址0xffffffffffff,CODE(代码)字段值为 0x09,SESSION-ID(会话ID)字段值为0x0000。PADI分组必须至少包含一个服务名称类型的标签(标签类型字段值为0x0101), 向接入集中器提出所要求提供的服务。
(2)接入集中器
  (2)接入集中器收到在服务范围内的PADI分组,发送PPPoE有效发现提供包(PADO)分组,以响应请求。其中CODE字段值为 0x07,SESSION-ID字段值仍为0x0000。PADO分组必须包含一个接入集中器名称类型的标签(标签类型字段值为0x0102),以及一个 或多个服务名称类型标签,表明可向主机提供的服务种类。
(3)主机选择一个合适的PADO分组
  (3)主机在可能收到的多个PADO分组中选择一个合适的PADO分组,然后向所选择的接入集中器发送PPPoE有效发现请求分组(PADR)。其中 CODE字段为0x19,SESSION_ID字段值仍为0x0000。PADR分组必须包含一个服务名称类型标签,确定向接入集线器(或交换机)请求的 服务种类。当主机在指定的时间内没有接收到PADO,它应该重新发送它的PADI分组,并且加倍等待时间,这个过程会被重复期望的次数。
(4)准备开始PPP会话
  (4)接入集中器收到PADR分组后准备开始PPP会话,它发送一个PPPoE有效发现会话确认PADS分组。其中CODE字段值为 0x65,SESSION-ID字段值为接入集中器所产生的一个唯一的PPPoE会话标识号码。PADS分组也必须包含一个接入集中器名称类型的标签以确 认向主机提供的服务。当主机收到PADS分组确认后,双方就进入PPP会话阶段。


(二)认证方式
  (方式1) 口令验证协议(PAP)
  PAP 是一种简单的明文验证方式。NAS(网络接入服务器,Network Access Server)要求用户提供用户名和口令,PAP以明文方式返回用户信息。

  (方式2) 挑战-握手验证协议(CHAP)
 CHAP是一种加密的验证方式,能够避免建立连接时传送用户的真实密码。NAS向远程用户发送一个挑战口令(challenge),其中包括会话ID和 一个任意生成的挑战字串(arbitrary challenge string)。远程客户必须使用MD5单向哈希算法(one-way hashing algorithm)返回用户名和加密的挑战口令,会话ID以及用户口令,称为Secret Password,其中用户名以非哈希方式发送。
     NAS根据用户名查找自己本地的数据库,得到和用户端进行加密所用的一样的密码,然后根据原来的挑战字串进行加密,将其结果与Secret Password作比较,如果相同表明验证通过,如果不相同表明验证失败。
     在整个连接过程中,CHAP将不定时的向客户端重复发送挑战口令,从而避免第3方冒充远程客户(remote client impersonation)进行攻击。


(三)PPP会话阶段
  用户主机与接入集中器根据在发现阶段所协商的PPP会话连接参数进行PPP会话。一旦PPPoE会话开始,PPP数据就可以以任何 其他的PPP封装形式发送。所有的以太网帧都是单播的。PPPoE会话的SESSION-ID一定不能改变,并且必须是发现阶段分配的值。   PPPoE还有一个PADT分组,它可以在会话建立后的任何时候发送,来终止PPPoE会话,也就是会话释放。它可以由主机或者接入集中器发送。当对 方接收到一个PADT分组,就不再允许使用这个会话来发送PPP业务。PADT分组不需要任何标签,其CODE字段值为0xa7,SESSION-ID字 段值为需要终止的PPP会话的会话标识号码。在发送或接收PADT后,即使正常的PPP终止分组也不必发送。PPP对端应该使用PPP协议自身来终止 PPPoE会话,但是当PPP不能使用时,可以使用PADT。

 


pppd是一个拨号客户端软件
下载地址:

pppd的配置文件在 /etc/ppp 目录下

chap-secrets 存放CHAP验证方式使用的用户名和密码
pap-secrets 存放PAP验证方式使用的用户名和密码
peers/dsl-provider 配置文件

dsl-provider文件内容如下:

noipdefault
usepeerdns
defaultroute
hide-password
lcp-echo-interval 20
lcp-echo-failure 3
connect /bin/true
noauth
persist
mtu 1492
noaccomp
default-asyncmap
plugin rp-pppoe.so eth0
user "12345"

chap-secrets文件内容如下:
12345 * 12345 *

命令行执行pppd的方法为:
pppd call dsl-provider

 

 

源码分析


重要的数据结构

这个结构体给出了特定的协议使用的调用方法
/*
 * 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) __P((int unit));
    /* Process a received packet */
    void (*input) __P((int unit, u_char *pkt, int len));
    /* Process a received protocol-reject */
    void (*protrej) __P((int unit));
    /* Lower layer has come up */
    void (*lowerup) __P((int unit));
    /* Lower layer has gone down */
    void (*lowerdown) __P((int unit));
    /* Open the protocol */
    void (*open) __P((int unit));
    /* Close the protocol */
    void (*close) __P((int unit, char *reason));
    /* Print a packet in readable form */
    int  (*printpkt) __P((u_char *pkt, int len,
     void (*printer) __P((void *, char *, ...)),
     void *arg));
    /* Process a received data packet */
    void (*datainput) __P((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) __P((void));
    /* Configure interface for demand-dial */
    int  (*demand_conf) __P((int unit));
    /* Say whether to bring up link for this pkt */
    int  (*active_pkt) __P((u_char *pkt, int len));
};

全局变量
/*
 * PPP Data Link Layer "protocol" table.
 * One entry per supported protocol.
 * The last entry must be NULL.
 */
struct protent *protocols[] = {
    &lcp_protent,
    &pap_protent,
    &chap_protent,
#ifdef CBCP_SUPPORT
    &cbcp_protent,
#endif
    &ipcp_protent,
#ifdef INET6
    &ipv6cp_protent,
#endif
    &ccp_protent,
    &ecp_protent,
#ifdef IPX_CHANGE
    &ipxcp_protent,
#endif
#ifdef AT_CHANGE
    &atcp_protent,
#endif
    &eap_protent,
    NULL
};

 

/*
 * This struct contains pointers to a set of procedures for
 * doing operations on a "channel".  A channel provides a way
 * to send and receive PPP packets - the canonical example is
 * a serial port device in PPP line discipline (or equivalently
 * with PPP STREAMS modules pushed onto it).
 */
struct channel {
 /* set of options for this channel */
 option_t *options;
 /* find and process a per-channel options file */
 void (*process_extra_options) __P((void));
 /* check all the options that have been given */
 void (*check_options) __P((void));
 /* get the channel ready to do PPP, return a file descriptor */
 int  (*connect) __P((void));
 /* we're finished with the channel */
 void (*disconnect) __P((void));
 /* put the channel into PPP `mode' */
 int  (*establish_ppp) __P((int));
 /* take the channel out of PPP `mode', restore loopback if demand */
 void (*disestablish_ppp) __P((int));
 /* set the transmit-side PPP parameters of the channel */
 void (*send_config) __P((int, u_int32_t, int, int));
 /* set the receive-side PPP parameters of the channel */
 void (*recv_config) __P((int, u_int32_t, int, int));
 /* cleanup on error or normal exit */
 void (*cleanup) __P((void));
 /* close the device, called in children after fork */
 void (*close) __P((void));
};


/*
 * Each FSM is described by an fsm structure and fsm callbacks.
 */
typedef struct fsm {
    int unit;   /* Interface unit number */
    int protocol;  /* Data Link Layer Protocol field value */
    int state;   /* State */
    int flags;   /* Contains option bits */
    u_char id;   /* Current id */
    u_char reqid;  /* Current request id */
    u_char seen_ack;  /* Have received valid Ack/Nak/Rej to Req */
    int timeouttime;  /* Timeout time in milliseconds */
    int maxconfreqtransmits; /* Maximum Configure-Request transmissions */
    int retransmits;  /* Number of retransmissions left */
    int maxtermtransmits; /* Maximum Terminate-Request transmissions */
    int nakloops;  /* Number of nak loops since last ack */
    int rnakloops;  /* Number of naks received */
    int maxnakloops;  /* Maximum number of nak loops tolerated */
    struct fsm_callbacks *callbacks; /* Callback routines */
    char *term_reason;  /* Reason for closing protocol */
    int term_reason_len; /* Length of term_reason */
} fsm;


typedef struct fsm_callbacks {
    void (*resetci)  /* Reset our Configuration Information */
  __P((fsm *));
    int  (*cilen)  /* Length of our Configuration Information */
  __P((fsm *));
    void (*addci)   /* Add our Configuration Information */
  __P((fsm *, u_char *, int *));
    int  (*ackci)  /* ACK our Configuration Information */
  __P((fsm *, u_char *, int));
    int  (*nakci)  /* NAK our Configuration Information */
  __P((fsm *, u_char *, int, int));
    int  (*rejci)  /* Reject our Configuration Information */
  __P((fsm *, u_char *, int));
    int  (*reqci)  /* Request peer's Configuration Information */
  __P((fsm *, u_char *, int *, int));
    void (*up)   /* Called when fsm reaches OPENED state */
  __P((fsm *));
    void (*down)  /* Called when fsm leaves OPENED state */
  __P((fsm *));
    void (*starting)  /* Called when we want the lower layer */
  __P((fsm *));
    void (*finished)  /* Called when we don't want the lower layer */
  __P((fsm *));
    void (*protreject)  /* Called when Protocol-Reject received */
  __P((int));
    void (*retransmit)  /* Retransmission is necessary */
  __P((fsm *));
    int  (*extcode)  /* Called when unknown code received */
  __P((fsm *, int, int, u_char *, int));
    char *proto_name;  /* String name for protocol (for messages) */
} fsm_callbacks;


状态机函数:

void fsm_init __P((fsm *));
void fsm_lowerup __P((fsm *));
void fsm_lowerdown __P((fsm *));
void fsm_open __P((fsm *));
void fsm_close __P((fsm *, char *));
void fsm_input __P((fsm *, u_char *, int));
void fsm_protreject __P((fsm *));
void fsm_sdata __P((fsm *, int, int, u_char *, int));
static void fsm_timeout __P((void *));
static void fsm_rconfreq __P((fsm *, int, u_char *, int));
static void fsm_rconfack __P((fsm *, int, u_char *, int));
static void fsm_rconfnakrej __P((fsm *, int, int, u_char *, int));
static void fsm_rtermreq __P((fsm *, int, u_char *, int));
static void fsm_rtermack __P((fsm *));
static void fsm_rcoderej __P((fsm *, u_char *, int));
static void fsm_sconfreq __P((fsm *, int));

链路控制层函数:

void lcp_open __P((int));
void lcp_close __P((int, char *));
void lcp_lowerup __P((int));
void lcp_lowerdown __P((int));
void lcp_sprotrej __P((int, u_char *, int)); /* send protocol reject */


pppd基本流程:

整个程序的主体实现是从主函数的LCP_OPEN()开始的,在这个函数里,调用了有限状态机FSM_OPEN(),而在FSM_OPEN() 中,callback指针指向了starting,于是就到了LCP_STARTING()函数来实现一个OPEN事件从而使得PPP状态准备从DEAD 到ESTABLISHED的转变。接下来,回到主函数,下面一步是调用START_LINK(),在此函数中会把一个串口设备作为PPP的接口,并把状态 转变为ESTABLISHED,然后调用lcp_lowerup()来告诉上层底层已经UP,lcp_lowerup()中调用 FSM_LOWERUP()来发送一个configure-request请求,再把当前状态设置为REQSENT状态,至此,第一个LCP协商的报文已 经发送出去。

main()
{
    //...基本配置
 lcp_open(0);  /* Start protocol */
 start_link(0);
 while (phase != PHASE_DEAD) {
     handle_events();
     get_input();
     if (kill_link)
  lcp_close(0, "User request");
     if (asked_to_quit) {
  bundle_terminating = 1;
  if (phase == PHASE_MASTER)
      mp_bundle_terminated();
     }
     if (open_ccp_flag) {
  if (phase == PHASE_NETWORK || phase == PHASE_RUNNING) {
      ccp_fsm[0].flags = OPT_RESTART; /* clears OPT_SILENT */
      (*ccp_protent.open)(0);
  }
     }
 }
}


接下来的流程实现主要就是在这个while循环中实现了。之前说过了我们已经发送了第一个配置协商报文,所以handle_events()主要就是做等 待接收数据包的时间处理了,在handle_events()里主要调用了两个函数一个是wait_input(),他的任务是等待并判断是否超时。
还有一个是calltimeout()他主要是做超时的处理。当等待并未超时而且有数据包过来,则调用整个PPPD中最重要的函数get_input() 函数。他主要接收过来的数据包并做出相应的动作。接下来就get_input()函数进行详细的说明,首先对包进行判断,丢弃所有不在LCP阶段和没有 OPENED状态的包,然后protop指针指向当前协议的input函数。于是就进入了LCP_INPUT(),同理LCP_INPUT()调用了 FSM_INPUT()对收到的包进行代码域的判断,判断收到的是什么包。假设比较顺利,我们收到的是CONFACK的包,于是调用 fsm_rconack()函数,在此函数中根据当前自身的状态来决定下一步的状态如何改变,这里我们假设也很顺利,已经发送完了configure- ack,因此我们把FSM当前状态变成了OPENED状态,并把callback指针指向UP.所以我们马上就调用LCP_UP()在那里我们又调用了 link_established()函数来进入认证的协商,或者如果没有认证则直接进入网络层协议。当然这里我们还是要认证的所有在 LINK_ESTABLISHED()里我们选择是利用何种认证方式是PAP,还是EAP,还是CHAP.假设我们这里采用CHAP而且是选择CHAP WITH PEER,意思是等待对端先发送CHALLENGE挑战报文。于是我们又调用了chap_auth_peer()函数,并等待接收挑战报文。于是从新又来 到handle_events()等待接收。再利用get_input()来接收包,在get_input()里这次调用chap_input(),再调 用FSM_INPUT(),在那里我们再对包的代码域进行判断,这次判断出是CHAP_CHALLENGE包,则我们要调用chap_respond() 函数来回应对端,继续等待对方的报文,再次利用CHAP_INPUT(),FSM_INPUT()来判断,如果是SUCCESS,则调用 chap_handle_status(),在这个函数里调用auth_withpeer_success函数,从而进入网络层阶段,调用 network_phase()函数。网络层的互动是从start_networks()开始的,如果在网络层阶段同时有CCP协议(压缩控制协议)则进 行压缩控制协议的协商,然后再进入正式的IPCP的协商,而IPCP的协商主要也是通过protop指针指向IPCP_OPEN()开始的。而 IPCP_OPEN()则是调用了FSM_OPEN(),在这里,首先发送一个configure-request包,然后和之前一样等待接收。经过几个 交互后最后调用NP_UP()完成网络层的协商,至此PPP链路可以承载网络层的数据包了。
阅读(759) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~