LLDP协议是一种标识交换机的协议,首先会想到交换机的名称,描述等,其实还有许多的内容,比如,端口的描述,ttl,管理ip,mac地址等。
抓取一个lldp报文,它的组成如下图所示
有目的地址(01:80:c2:00:00:0e)和源地址(00:26:f8:99:00:77),还有协议类型0x88cc这是报文的协议标识,比如0x0800是ipv4,0x8100是vlan等等。往下就是tlv了,比如Chassis subtype、port subtype、Time to Live(ttl)、port description等等
这些数据都是以字符的形式放入数组中的
一个一个地分析
一、目的地址是01:80:c2:00:00:0X形式,这个是管理帧,可以参考stp报文的目的地址,意思是可以通过block或discard状态的端口的报文,这类报文是会进入到cpu进行处理的。在开启一些环网协议后这些报文和普通的报文的处理方式不同,所以就会有一个有趣的处理方式,刚开始我写的时候没有注意到这个,我在内存空间里面将mac地址作为标记,不同的mac地址就代表的一台设备,后来在与其他厂家的对接中发现人家的交换机将通过阻塞端口的lldp报文也加入进来了,简单来说就是lldp表中出现了相同的mac地址的设备但是它们的port describe不同。为了说明我在此详细说明,如下图所示的两台交换机,它们开启stp服务后如图所示
根据stp协议的工作方式假设c和e阻塞,而d是转发端口那么可以相当于c和e是断开的,这样普通报文是不会通过这个端口的,这样也就不会有广播风暴了,但是lldp作为管理报文是可以通过的这些端口的,也就是说A交换机会收到3个mac地址,设备描述,型号ttl等等相同但是端口不同的lldp报文,如果只是单纯的将mac地址作为标识那么在lldp表中就只会只有一个lldp设备了并且由于接收和处理的原因它的端口还会一直变化,那么如果将其中的链表(或内存保存格式,不一定是用链表写的)中的mac地址和端口一起作为标识不就可以了吗?确实如此,通过这样的编写代码可以和其他厂家的兼容。
二、源地址即交换机的地址,这个地址可以根据需要更改其实就是你交换机的源mac
三、TLV的说明如下图所示
其中前面的几个是必须的数据,不过一般来说会将上图的所有的tlv写进代码里。当然只有在发送shutdown帧时会直接使用上面的几个。而且会让ttl=0,那样可以直接将该设备老化从而将其从交换机的lldp表中删除。
PS:在交换机连接PC电脑时,电脑也会发送lldp报文,但是它只有前面的几个内容。可以编写识别这些报文的代码,然后不识别这类报文。在某些老旧的交换机发的报文也只会包含前面几个TLV建议和电脑的lldp报文一样让交换机丢弃。当然了不同厂家的做法不一样的,咱们家的就不丢弃,毕竟人家虽然老不过也还是标准的报文,比如有些厂家的交换机不能接收到wifi的lldp报文这让我觉得没必要删除这些报文,无线路由器就没有被链路发现的必要了吗?
TLV一般由三部分或四部分组成当然具体组成还是参考说明文档
A、四部分,其实就是比三部分构成的多了一个子类型
上图是在88cc协议标记后接的tlv
它的类型是02,但是wireshark显示的是1,其实这个值是经过了左移了的,所以还原的时候需要右移一位2>>1=1,长度是07是指后面接的04-00-26-f8-99-00-77这7个数组值(也可以叫字符串),子类型是04请参考lldp手册中的chassis子类型的值,如下图所示
其中4对应的就是mac地址,后面接的就是内容了,mac地址那么就是6个16进制的数组成的所以它是由类型04+mac地址构成的7个字符的长度。它就是源mac地址。我们在发送和接收这个类型的时候可以用对应的函数在数组中一个一个的处理,注意此处的值是char型的数0对应的是ascii码的0值,所以将其变成字符或字符串形式进入传输或者打印复制的时候,要注意转换类型,比如同样是0,0是ascii码的第一个 BLANK NULL的非打印字符,而字符‘0’对应是值是48。
B、三部分
如上图所示其值的含义与上述有些不同,它是系统名称,没有子类型
第一个值是0x0a>>1=5即它的类型,第二个值代表长度为5,系统名称没有子类型。即后面的值是它的名称,这些值是16进制的,找到它们对应的ascii码,0x54,0x39,0x30,x034的十进制是84,57,48,48,52查下表可知即为T9004
总结:报文的组成已经说完了,比较简单只需对应的位置编写即可
接下来是重点,即状态机,但是我讲得比较地简单,只说明最基本的形式,比如有只接收不发送的状态,只发送不接收状态、即接收又发送的状态、即不接收也不发送状态(关闭状态)等等,我在网上找一个图,上面有什么初始化啊,什么发送状态啊,接收状态啊,状态改变时,空闲状态的处理啊,初学者可能一脸懵逼,这是啥,这咋写啊。其实不难。移植的时候发现人家写的库函数不过是用#define的高级用法写的罢了。不过最好还是自己看完人家的代码后感悟才是最大的。那么接下来就是代码分析了,这个其实我懒得说明,代码多看看就行了。不过留个备份也是极好的。
1、新建一个线程。学过单片机的可以理解为一个while(1)的循环体,只不过在linux等操作系统中可以用实时性等操作可以有多个while(1)的程序在运行。这个while(1)有一个1s的延时,即每一秒运行一次该程序。
代码如下:
if (pthread_create (&tid_lldp_rx, NULL, (void *) LldpBpduHandlerTask,
NULL) < 0)
printf ("lldp read protocol packets pthread created error\r\n");
if (pthread_create (&tid_lldp_rx, NULL, (void *) LldpSetHandlerTask, NULL) < 0)
printf ("lldp read protocol packets pthread created error\r\n");
if(pthread_create(&timer_lldp_rx, NULL, (void *)LldpTimerHandlerTask, NULL)<0){
printf("lldp read protocol packets pthread created error\r\n");
}
前面两个线程一个是接收lldp报文,一个打包lldp报文,处理的话在其他的地方,这里就不展开分析了,第三个就是计时器,这个计时器包含了一个滴答计时器,那么每过1秒就会处理一次,简单来说就是经过1s所有的lldp表中的设备的ttl减1s,但是也会运行lldp_port_timers_tick函数一次。
void lldp_port_timers_tick (lldp_sm_t *sm)
{
/* Timer handling */
if (sm->timers.txShutdownWhile > 0)
sm->timers.txShutdownWhile--;
if (sm->timers.txDelayWhile > 0)
sm->timers.txDelayWhile--;
if (sm->timers.txTTR > 0)
sm->timers.txTTR--;
/* step state machines */
lldp_sm_step (sm);
}
上述代码分析一下就是 sm即state machine(状态机)中的txShutdownWhile,txDelayWhile,txTTR中要是大于0就自减,后面有代码如果这三个值中有一个为0时就发送lldp报文来刷新对方的lldp设备表。后面还有一个lldp_sm_step的函数。分析一下。
lldp_sm_step分析
void lldp_sm_step (lldp_sm_t *sm)
{
lldp_u8_t prev_tx;
lldp_u8_t prev_rx;
CM_LLDP_SM_LOCK;
do
{
/* register previous states of state machines */
prev_tx = sm->tx.state;
prev_rx = sm->rx.state;
/* run state machines */
SM_STEP_RUN (TX);
SM_STEP_RUN (RX);
/* repeat until no changes */
} while ( prev_tx != sm->tx.state ||
prev_rx != sm->rx.state );
CM_LLDP_SM_UNLOCK;
}
使用一个信号量锁住数据,防止数据出错,中间的do_while循环是用来稳定状态的,状态稳定后退出,调用了两个函数,一个是发送TX,一个是接收RX,那么就非常的简单了,找到这两个函数的原型, SM_STEP_RUN
TX如下图所示,我只分析switch中的代码,
switch (sm->tx.state)
{
case TX_LLDP_INITIALIZE:
//printf("enter TX_LLDP_INITIALIZE 1\n");
if ( (sm->adminStatus == LLDP_ENABLED_RX_TX) ||
(sm->adminStatus == LLDP_ENABLED_TX_ONLY) )
{
//printf("enter TX_LLDP_INITIALIZE 2\n");
SM_ENTER (TX, TX_IDLE);
}
break;
case TX_IDLE:
if ( (sm->adminStatus == LLDP_DISABLED) ||
(sm->adminStatus == LLDP_ENABLED_RX_ONLY) )
{
//TASK(SUB_TASK_ID_LLDP_TX, SM_ENTER(TX, TX_SHUTDOWN_FRAME));
//printf("enter TX_IDLE 1\n");
SM_ENTER (TX, TX_SHUTDOWN_FRAME);
}
else if ( (sm->timers.txDelayWhile == 0) && ( (sm->timers.txTTR == 0) ||
(sm->tx.somethingChangedLocal == LLDP_TRUE) ) )
{
//TASK(SUB_TASK_ID_LLDP_TX, SM_ENTER(TX, TX_INFO_FRAME));
//printf("enter TX_IDLE 2\n");
SM_ENTER (TX, TX_INFO_FRAME);
}
else if (sm->tx.re_evaluate_timers)
{
//printf("enter TX_IDLE 3\n");
SM_ENTER (TX, TX_IDLE);
}
//printf("enter TX_IDLE 4\n");
break;
case TX_SHUTDOWN_FRAME:
//printf("enter TX_SHUTDOWN_FRAME 1\n");
if (sm->timers.txShutdownWhile == 0)
{
//printf("enter TX_SHUTDOWN_FRAME 2\n");
SM_ENTER (TX, TX_LLDP_INITIALIZE);
}
break;
case TX_INFO_FRAME:
/* UCT */
//printf("enter TX_INFO_FRAME 1\n");
SM_ENTER (TX, TX_IDLE);
break;
default:
//printf("enter TX_default 1\n");
break;
}
从名称上可以得知有初始化,空闲处理是否发帧,发送停止帧,发送lldp帧。
1、初始化
初始化前文提到的txShutdownWhile,txDelayWhile,txTTR等一系列的数据
2、空闲是否发帧的处理
前文也有提到首先判断lldp是否已经关闭了,如果关闭了就发送停止帧,如果传输时间到了也就是说要给对端交换机发送lldp报文了,也就是前文提到的txShutdownWhile,txDelayWhile,txTTR这三个值有一个为零
3、发送停止帧
这里和上述的关闭一起说明,意义是一样的,其实非常的简单发送一个ttl=0的lldp报文即可,那么对端交换机就会将ttl刷新,那么经过处理对端的交换机会将此设备从lldp表中删除。
4、发送lldp帧
将目的地址0x01:0x80:0xC2:0x00:0x00:0x0E;自身的mac地址以及二层需要的数据以及88cc类型标识还有后续的tlv数据打包发送出去。
RX和TX的机制类似都是状态机,这里就不再赘述了。
总结:lldp状态机程序编写的几个核心
核心1是ttl即存活时间,其实就是交换机中的老化时间。一旦时间到了,该设备会被表删除。
核心2是如果交换机的状态发生了变化状态机就会进入到另一个状态中,其实简单来说就一个switch语句,如果发生了变化就进入到另一个case中去了。比如在idle状态(即空闲状态)时将它关闭,交换机的lldp状态立马进入shutdown状态,即发送一个ttl=0的报文,对端交换机在收到这个报文后将其ttl置0那么它会立马老化,对端交换机将它从lldp设备表中删除。
核心3是间隔发送时间,其实就是为了保持一直在对端交换机表中,防止老化,会每隔一段时间会自动发送lldp报文。这个报文传输时间应该小于间隔时间的1/4。这里有间隔时间,存活时间乘数,重初始化延时,传输延时等参数。非常简单有兴趣的话自行百度,这里不展开讨论。
最后:
将这些信息与mib库联系起来。这里解释下mib和snmp。用户通过串口和网页web还有snmp总共有3种方式来访问和控制交换机,当然了telnet或ssh也勉强算一种了。而snmp其实是通过网管来控制交换机,这些软件可以自动画出拓扑图来让人更好地管理交换机。而mib库是一个数据节点,通过MG_SOFT软件来得到节点和数据,网管软件工程师就可以利用这些数据来设计网管软件了。
如图所示,放上一些参考代码这是mib库的编写例子
mib库的编写非常简单,将节点定义好就好了,主要是将数据放入对应的位置,因为数据是放在内存中的只要将结构体中的值一个一个的取出来就好了,也非常简单。
处理好的效果如图所示