FantinOcean

Share


  • Home

  • Archives

  • Tags

To Visiters

Posted on 2018-05-11

My very first post – To Visiters

Who Am I

You can find me in…

  • Facebook
  • Twitter
  • Instagram
  • Weibo
  • Bilibili
  • Email

Facebook, Twitter, Insta, and Weibo are set up only as my social media, you can follow me if interested, but I won’t guarantee the quality.
Bilibili is where I upload my Vlogs.
In the end, don’t hesitate to Email me.

What I gonna do

The reason why I set up this blog is that I want to share my working experience with freshmen in SDN, Wireless Security, OpenWrt and so on. Because when I first stepped foot in these areas, I couldn’t always find the useful information online. There full of repeated tutorials and blogs, and what important is that most of the tutorials are not suitable for freshmen in engineering, because they assume the readers have equipped enough basic knowledge.
So I decided to share the journey of me on this blog, where I will focus more on how to start up the engineering and how to get useful information.
Hope it will be helpful to you.

DEMO--Live555取流与ffmpeg解码——从零开始

Posted on 2019-07-26

本片文章主要总结如何利用live555库取流,再使用ffmpeg库将H264解码为YUV420。将这个过程制作为一个demo。因为是第一次涉及视频流传输和解码,所以这种“从零开始”的开发过程可能比较适合新手。
live555是一个基于RTP/RTSP等协议开源的视频流库。而ffmpeg更像是一个视频处理工具。

编译环境配置

live555部分

live555的结构还是比较清晰的,如果需要开发一个客户端,主要关注“testProgs”和“testRtspClient”这两个文件夹即可,其中都给出了一些开发实例和源代码。
刚开始我以为client就是testRtspClient中的同名.cpp文件,后来发现,在testProgs文件夹中的 testRTSPClient.cpp 才更适合开发。
所以我复制了 testRTSPClient.cpp 的代码,放在自己的demo中,把他改名为client.cpp 进行二次开发。

Makefile可以直接参考testRtspClient文件夹中的Makefile,基本是testProgs的缩简版。
可以先从Makefile分析一下编译构成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
INCLUDES = -I../UsageEnvironment/include -I../groupsock/include -I../liveMedia/include -I../BasicUsageEnvironment/include -I../openRTSP/include

TEST_RTSP_CLIENT_OBJS = testRtspClient.$(OBJ)


USAGE_ENVIRONMENT_DIR = ../UsageEnvironment
USAGE_ENVIRONMENT_LIB = $(USAGE_ENVIRONMENT_DIR)/libUsageEnvironment.$(libUsageEnvironment_LIB_SUFFIX)
BASIC_USAGE_ENVIRONMENT_DIR = ../BasicUsageEnvironment
BASIC_USAGE_ENVIRONMENT_LIB = $(BASIC_USAGE_ENVIRONMENT_DIR)/libBasicUsageEnvironment.$(libBasicUsageEnvironment_LIB_SUFFIX)
LIVEMEDIA_DIR = ../liveMedia
LIVEMEDIA_LIB = $(LIVEMEDIA_DIR)/libliveMedia.$(libliveMedia_LIB_SUFFIX)
GROUPSOCK_DIR = ../groupsock
GROUPSOCK_LIB = $(GROUPSOCK_DIR)/libgroupsock.$(libgroupsock_LIB_SUFFIX)
OPENRTSP_DIR = ../openRTSP
OPENRTSP_LIB = $(OPENRTSP_DIR)/libRtspClient.$(libopenrtsp_LIB_SUFFIX)
LOCAL_LIBS = $(OPENRTSP_LIB) $(LIVEMEDIA_LIB) $(GROUPSOCK_LIB) \
$(BASIC_USAGE_ENVIRONMENT_LIB) $(USAGE_ENVIRONMENT_LIB)
LIBS = $(LOCAL_LIBS) $(LIBS_FOR_CONSOLE_APPLICATION)



testRTSPClient$(EXE): $(TEST_RTSP_CLIENT_OBJS) $(LOCAL_LIBS)
$(LINK)$@ $(CONSOLE_LINK_OPTS) $(TEST_RTSP_CLIENT_OBJS) $(LIBS) -lpthread

可以看到动态库就是UsageEnvironment,BasicUsageEnvironment,liveMedia,groupsock和openRTSP。所以写demo的时候需要把这几个文件夹中的include和.a文件包含进来。
文件结构:
.
├── BasicUsageEnvironment
│ ├── include
│ └── libBasicUsageEnvironment.a
├── groupsock
│ ├── include
│ └── libgroupsock.a
├── liveMedia
│ ├── include
│ └── libliveMedia.a
├── openRTSP
│ ├── include
│ └── libRtspClient.a
└── UsageEnvironment
├── include
└── libUsageEnvironment.a

这样就完成了live555的环境配置。

ffmpeg部分

ffmpeg就没这么简单了,ffmpeg的库很多,互相依赖复杂。因为这个demo中只需要将h264解码为YUV420,所以理论上只需要用到libavformat、libavcodec、libavutil这三个库。但实际上,我最后还加上了libswresample,一共四个库。
在网上下好源码包之后,应该先在文件夹下执行
./configure
make
make install
确保几个库中都编译好动态库,方便直接拷贝引用。
我的方式是直接复制上诉4个文件夹在demo文件夹中,文件夹除了包含头文件还应该包含动态库。

Makefile修改

整个demo的文件结构是这样的:
.
├── libavcodec
├── libavformat
├── libavutil
├── libswresample
├── live
├── client.cpp

live中是上述live555的库,client.cpp 是主函数。

因为ffmepg的头文件引用都是使用 #include <libavcodec/avcodec.h>这样的方式引用,所以我把主函数和几个库文件夹放在同一个根目录下。
live555通过#include "liveMedia.hh"引用。所以live555需要分别添加库的文件地址,而ffmpeg只需要添加根地址。

Makefile在上述testRTSPClient的Makefile中修改。

  1. INCLUDES 中添加ffmpeg几个库的根地址。
    这里的根地址就是当前地址。
    在原来的INCLUDES 后面添加 -I./ 即可。
  2. live库的引用则需要在原来的基础上加上live/。需要注意的是,还是因为目录结构的不同,原版Makefile的INCLUDES 后面有两个.. ,在我的目录结构下,只需要一个。大家要根据自己的结构修改。
  3. 最终版本:
    INCLUDES = -I./live/UsageEnvironment/include -I./live/groupsock/include -I./live/liveMedia/include -I./live/BasicUsageEnvironment/include -I./live/openRTSP/include -I./

除了-I的修改,还有链接库需要修改。
我按照live555的写法,把ffmpeg的链接库添加在后面,具体是:

1
2
3
4
5
6
7
8
9
10
11
12
LIBAVCODEC_DIR = ./libavcodec
LIBAVCODEC_LIB = $(LIBAVCODEC_DIR)/libavcodec.$(LIB_SUFFIX)
LIBAVUTIL_DIR = ./libavutil
LIBAVUTIL_LIB = $(LIBAVUTIL_DIR)/libavutil.$(LIB_SUFFIX)
LIBAVFORMAT_DIR = ./libavformat
LIBAVFORMAT_LIB = $(LIBAVFORMAT_DIR)/libavformat.$(LIB_SUFFIX)
LIBSWRESAMPLE_DIR = ./libswresample
LIBSWRESAMPLE_LIB = $(LIBSWRESAMPLE_DIR)/libswresample.$(LIB_SUFFIX)

LOCAL_LIBS = $(OPENRTSP_LIB) $(LIVEMEDIA_LIB) $(GROUPSOCK_LIB) \
$(BASIC_USAGE_ENVIRONMENT_LIB) $(USAGE_ENVIRONMENT_LIB)\
$(LIBAVCODEC_LIB) $(LIBAVFORMAT_LIB) $(LIBAVUTIL_LIB) $(LIBSWRESAMPLE_LIB)

基本就是加上lib地址,然后在local_libs里面添加就可以。

这样,demo的环境就搭好了,可以修改代码,然后make编译。

代码修改

live555解读

live555的代码读起来真的是新手不友好,但是修改起来还是方便的。
找到main函数,首先初始化环境env

1
2
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
UsageEnvironment* env = BasicUsageEnvironment::createNew(*scheduler);

调用OpenUrl函数打开url(rtsp://摄像头的ip, 而且url是支持用户名密码认证的:rtsp://admin:password@ip)

1
2
3
for (int i = 1; i <= argc-1; ++i) {
openURL(*env, argv[0], argv[i]);
}

然后就开始循环执行env下的task。

1
env->taskScheduler().doEventLoop(&eventLoopWatchVariable);

task包括什么,基本就是所有调用了UsageEnvironment* env的函数。
因为我们的目的是通过live收到一帧视频并解码,所以可以不关注连接建立的过程,主要关注收到视频帧部分的操作。

这里有一个类比较重要:

1
2
3
DummySink* createNew(UsageEnvironment& env,
MediaSubsession& subsession, // identifies the kind of data that's being received
char const* streamId = NULL); // identifies the stream itself (optional)

他相当于提供一个池子,接受视频帧。子函数包括:

1
2
3
4
5
6
DummySink* DummySink::createNew(UsageEnvironment& env, MediaSubsession& subsession, char const* streamId) 

void DummySink::afterGettingFrame(void* clientData, unsigned frameSize, unsigned numTruncatedBytes,
struct timeval presentationTime, unsigned durationInMicroseconds)

Boolean DummySink::continuePlaying()

分别是构造——处理——继续的过程。

如果我们需要对视频帧处理,直接将处理函数加在afterGettingFrame函数末尾,continuePlaying()前面即可。

H264 与 sps pps帧

live555接受到的是H264的裸流,要获取h264格式,需要在原始数据的高为加包头。而原始数据保存在DummySink的成员函数fReceiveBuffer中,大小保存在freamesize里。

1
2
3
4
unsigned char *SaveData = new unsigned char[DUMMY_SINK_RECEIVE_BUFFER_SIZE + 4];
char head[4] = {0x00, 0x00, 0x00, 0x01};
memcpy(SaveData, head, 4);
memcpy(SaveData+4, fReceiveBuffer,frameSize);

DUMMY_SINK_RECEIVE_BUFFER_SIZE根据摄像头数据定义,可以定义大一些。

#define DUMMY_SINK_RECEIVE_BUFFER_SIZE 409600

解码为YUV420需要依赖sps和pps帧,在live中,这两帧分别以数组形式保存在SPropRecord中,通过下面的代码将他们取出来。

1
2
3
4
5
unsigned int SPropRecords = -1;
SPropRecord *p_record = parseSPropParameterSets(fSubsession.fmtp_spropparametersets(), SPropRecords);
//sps pps 以数组的形式保存SPropRecord中
SPropRecord &sps = p_record[0];
SPropRecord &pps = p_record[1];

这样,我们就获得了h264包和sps pps帧数据。

ffmpeg调用

我将解码部分代码写在decodeyuv()函数中。

1
2
int decoderyuv(unsigned char * inbuf, int read_size, SPropRecord &sps, SPropRecord &pps)
{ }

h264包、包大小、sps和pps帧作为函数输入。

首先,需要对解码器初始化。但是考虑到解码过程中是收到一帧解码一帧,但对解码器的初始化只需要做一次,否则会丢失sps和pps帧和其他环境变量的依赖。

所以,我将以下初始化代码放在主函数中,将变量作为全局变量声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AVCodec *codec;
AVCodecContext *c = NULL;
AVFrame *frame;
AVCodecParserContext *avParserContext;

avcodec_register_all();
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if( NULL == codec )
{
fprintf(stderr,"Codec not found\n");
exit(1);
}

c = avcodec_alloc_context3(codec);
if(NULL == c)
{
fprintf(stderr,"Could not allocate video codec context\n");
exit(1);
}

avParserContext = av_parser_init(AV_CODEC_ID_H264);
if( NULL == avParserContext)
{
fprintf(stderr,"Could not init avParserContext\n");
exit(1);
}

if(avcodec_open2(c,codec, NULL) < 0)
{
fprintf(stderr,"Could not open codec\n");
exit(1);
}

frame = av_frame_alloc();
if(NULL == frame)
{
fprintf(stderr,"Could not allocate video frame\n");
exit(1);
}

以下是decodeyuv函数代码。

  • 首先将sps和pps帧添加到context的 extradata中。
  • 然后将extradata(sps 和pps帧数据)添加到h264数据前,重构buffer。
  • 使用解码器解码,解码后的数据将存储在frame->data中,而got_frame变量记录了解码数据大小。
  • 将解码后的YUV数据存储到文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
int decoderyuv(unsigned char * inbuf, int read_size, SPropRecord &sps, SPropRecord &pps)
{
int got_frame;
if(c->extradata == NULL)
{
int totalsize = 0;
unsigned char* tmp = NULL;
unsigned char nalu_header[4] = {0x00, 0x00, 0x00, 0x01};
totalsize = 8 + sps.sPropLength + pps.sPropLength;
//在每个sps 和pps 之前加上startcode
tmp = (unsigned char*)realloc(tmp, totalsize);
memcpy(tmp, nalu_header, 4);
memcpy(tmp + 4, sps.sPropBytes, sps.sPropLength);
memcpy(tmp + 4 + sps.sPropLength, nalu_header, 4);
memcpy(tmp + 4 + sps.sPropLength + 4, pps.sPropBytes, pps.sPropLength);
//printf("sps len:%d, pps len:%d\n", sps.sPropLength, pps.sPropLength);
//将 sps 和pps 的数据给ffmpeg的h264解码器上下文
c->extradata_size = totalsize;
c->extradata = tmp;
}
//重构buf
unsigned char * poutbuf = (unsigned char*)malloc(INBUF_SIZE*2 + AV_INPUT_BUFFER_PADDING_SIZE);
int buf_size = read_size + c->extradata_size;
memcpy(poutbuf, c->extradata, c->extradata_size);
memcpy(poutbuf + c->extradata_size, inbuf, read_size);

//解码器解码
AVPacket avpkt = {0};
av_init_packet(&avpkt);
avpkt.data = poutbuf;
avpkt.size = read_size + c->extradata_size;
time_t start ,end;
start = clock();
int decode_len = avcodec_decode_video2(c,frame, &got_frame, &avpkt);
end = clock();
printf("decoded frame used %d\n",end - start);
if(decode_len < 0)
fprintf(stderr,"Error while decoding frame \n");
if(got_frame)
{
int width = frame->width;
int height = frame->height;
fprintf(stderr,"decode success\n");
printf("width:%d, height:%d\n", frame->width, frame->height);
if( savefile)
{
yuv_fp = fopen(filename, "a+b");
解码后,存储的文件
if(NULL == yuv_fp)
{
fprintf(stderr,"Open file failed\n");
exit(1);
}
unsigned char* yuv_buf = (unsigned char*)malloc(width * height *1.5);//可以放在while循环外面,这样就不用每次都申请,释放了
//unsigned char* outbuff = (unsigned char*)malloc(width * height *1.5);
int i = 0;
//把解码出来的数据存成YUV数据,方便验证解码是否正确
for(i = 0; i < height; i++)
{
memcpy(yuv_buf + width * i, frame->data[0] + frame->linesize[0]*i, width);
if(i < height >> 1)
{
memcpy(yuv_buf + width * height + width *i / 2, frame->data[1] + frame->linesize[1]*i, width / 2);
memcpy(yuv_buf + width * height * 5 /4 + width * i / 2 ,frame->data[2] + frame->linesize[2]*i, width / 2);
}
}
fwrite(yuv_buf, sizeof(unsigned char),width * height * 1.5 ,yuv_fp);
free(yuv_buf);
fclose(yuv_fp);
yuv_fp = NULL;
}
}
free(poutbuf);
//记得释放变量的空间
return 0;
}

至此,就可以完成一个从live555取流,用ffmpeg解码为YUV,再将视频存储的demo。

基于CoAP的AP无线网络数据传输设计

Posted on 2019-06-21

这篇内容是关于AP的改造设计,该设计可分为AP网络扫描功能设计和AP数据传输设计。即AP扫描网络环境数据包,同时将网络数据通过CoAP协议上传。由此,AP可作为网络安全代理起,实时监控无线网络环境数据。

功能模块:

  1. 网络扫描功能

  2. 基于CoAP协议传输功能

AP网络环境扫描实现

核心实现

该功能模块参考AP开源应用iwcap,packet可在OpenWrt wiki上查询, github源代码。

其核心实现是通过绑定本地无线网卡,建立socket通信通道,完成无线数据包抓取。

具体实现代码:

1
2
3
capture_sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); #简历socket通道
bind(capture_sock, (struct sockaddr *)&local, sizeof(local)); # 绑定网卡与通道
pktlen = recvfrom(capture_sock, pktbuf, sizeof(pktbuf), 0, NULL, 0); #从通道抓包

socket通信

在Unix/Linux系统中,为了统一对各硬件的操作,简化接口,不同硬件设备可以被看成一个文件,等同于对磁盘上普通文件的操作。

用socket()函数创建一个网络连接,其返回值就是文件描述符。根据文件描述符,我们可以使用普通的文件操作来传输数据。

相关函数:accept, bind, connect, listen…

头文件:

1
2
#include <sys/types.h>
#include <sys/socket.h>

函数定义:

1
int socket(int domain, int type, int protocol);

参数domain指定使用何种地址类型,常见的包括:

1
2
3
4
5
6
7
8
9
10
PF_UNIX/PF_LOCAL/AF_UNIX/AF_LOCAL UNIX #进程通信协议
PF_INET?AF_INET Ipv4 #网络协议
PF_INET6/AF_INET6 Ipv6 #网络协议
PF_IPX/AF_IPX IPX-Novell #协议
PF_NETLINK/AF_NETLINK #核心用户接口装置
PF_X25/AF_X25 #ITU-T X.25/ISO-8208 协议
PF_AX25/AF_AX25 #业余无线AX. 25 协议
PF_ATMPVC/AF_ATMPVC #存取原始 ATM PVCs
PF_APPLETALK/AF_APPLETALK #appletalk (DDP)协议
PF_PACKET/AF_PACKET #初级封包接口

参数type:

1
2
3
4
5
6
SOCK_STREAM #提供双向连续且可信赖的数据流, 即TCP. 支持 OOB 机制, 在所有数据传送前必须使用connect()来建立连线状态.
SOCK_DGRAM #使用不连续不可信赖的数据包连接
SOCK_SEQPACKET #提供连续可信赖的数据包连接
SOCK_RAW #提供原始网络协议存取
SOCK_RDM #提供可信赖的数据包连接
SOCK_PACKET #提供和网络驱动程序直接通信. protocol 用来指定socket 所使用的传输协议编号, 通常此参考不用管它, 设为0 即可.

在此应用中,协议指定从数据链路中接收分组。ETH_P_ALL定义于 /usr/include/linux/if_ether.h中

1
2
3
4
5
6
7
8
9
10
#define ETH_P_ALL 0x0003
#define ETH_P_LOOP 0x0060 /* Ethernet Loopback packet */
#define ETH_P_PUP 0x0200 /* Xerox PUP packet */
#define ETH_P_PUPAT 0x0201 /* Xerox PUP Addr Trans packet */
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
#define ETH_P_X25 0x0805 /* CCITT X.25 */
#define ETH_P_ARP 0x0806 /* Address Resolution packet */
#define ETH_P_BPQ 0x08FF /* G8BPQ AX.25 Ethernet Packet [ NOT AN
#define ETH_P_IEEEPUP 0x0a00 /* Xerox IEEE802.3 PUP packet */
#define ETH_P_IEEEPUPAT 0x0a01 /* Xerox IEEE802.3 PUP Addr Trans packe[t*/

参考链接1)

参考链接2

CoAP客户端/服务器在AP上的C语言实现

协议参考RFC7252

CoAP以各种语言的具体实现均有版本,如有需要可以在此博客查看。

我在实现上参考了microcoap中的coap.c, coap.h 和 endpoint.h。

传输方案设计

将CoAP客户端部署在AP上,CoAP服务器部署在云平台。

CoAP client 主动向 Server发送CON消息,完成认证和连接。Server向Client发送ACK消息,消息option中存放”start” / “stop” 控制程序开始 / 结束。

程序开始,AP抓取网络数据包,数据内容存放在CoAP数据包 payload中,并以NON类型上传至Server。具体传输过程如图:

接下来简单介绍客户端和服务器代码实现的核心内容。

CoAP包结构

microcoap代码中的核心函数主要包括:coap_build、coap_parse、coap_handle_req 分别执行数据包构建、数据包解析和数据包回复的功能。熟悉CoAP协议的包结构后理解比较简单,下面主要介绍一下CoAP协议的包结构。

【Ver】 版本编号,指示CoAP协议的版本号。类似于HTTP 1.0 HTTP 1.1。版本编号占2位,取值为01B。

【T】报文类型,CoAP协议定了4种不同形式的报文,CON报文,NON报文,ACK报文和RST报文。

【TKL】CoAP标识符长度。CoAP协议中具有两种功能相似的标识符,一种为Message ID(报文编号),一种为Token(标识符)。其中每个报文均包含消息编号,但是标识符对于报文来说是非必须的。

【Code】功能码/响应码。Code在CoAP请求报文和响应报文中具有不同的表现形式,Code占一个字节,它被分成了两部分,前3位一部分,后5位一部分,为了方便描述它被写成了c.dd结构。其中0.XX表示CoAP请求的某种方法,而2.XX、4.XX或5.XX则表示CoAP响应的某种具体表现。

【Message ID】报文编号

【Token】标识符具体内容,通过TKL指定Token长度。

【Option】报文选项,通过报文选项可设定CoAP主机,CoAP URI,CoAP请求参数和负载媒体类型等等。

【1111 1111B】CoAP报文和具体负载之间的分隔符。

Code部分

Code部分被分成了两部分,为了便于阅读,Code被描述为c.dd形式。具体内容可参考RFC7252 #12.1.1 Method Codes

请求

​ 在CoAP请求中,Code被定义为CoAP请求方法,这些方法有GET、POST、PUT和DELETE,这些方法和HTTP协议非常相似。

​ (0.01)GET方法——用于获得某资源

​ (0.02)POST方法——用于创建某资源

​ (0.03)PUT方法——用于更新某资源

​ (0.04)DELETE方法——用于删除某资源

响应

​ 在CoAP响应中,Code被定义为CoAP响应码,类似于HTTP 200 OK等等。

​ (2.01)Created

​ (2.02)Deleted

​ (2.03)Valid

​ (2.04)Changed

​ (2.05)Content。类似于HTTP 200 OK

​

​ (4.00)Bad Request 请求错误,服务器无法处理。类似于HTTP 400。

​ (4.01)Unauthorized 没有范围权限。类似于HTTP 401。

​ (4.02)Bad Option 请求中包含错误选项。

​ (4.03)Forbidden 服务器拒绝请求。类似于HTTP 403。

​ (4.04)Not Found 服务器找不到资源。类似于HTTP 404。

​ (4.05)Method Not Allowed 非法请求方法。类似于HTTP 405。

​ (4.06)Not Acceptable 请求选项和服务器生成内容选项不一致。类似于HTTP 406。

​ (4.12)Precondition Failed 请求参数不足。类似于HTTP 412。

​ (4.15)Unsuppor Conten-Type 请求中的媒体类型不被支持。类似于HTTP 415。

​ (5.00)Internal Server Error 服务器内部错误。类似于HTTP 500。

​ (5.01)Not Implemented 服务器无法支持请求内容。类似于HTTP 501。

​ (5.02)Bad Gateway 服务器作为网关时,收到了一个错误的响应。类似于HTTP 502。

​ (5.03)Service Unavailable 服务器过载或者维护停机。类似于HTTP 503。

​ (5.04)Gateway Timeout 服务器作为网关时,执行请求时发生超时错误。类似于HTTP 504。

​ (5.05)Proxying Not Supported 服务器不支持代理功能。

在程序中,code占8位,具体编程实现:

1
2
3
#define MAKE_RSPCODE(clas, det)((clas << 5) | det)

COAP_RSPCODE_CONTENT = MAKE_RSPCODE(2,5)

Option部分

CoAP支持多个Option,CoAP的Option的表示方法比较特殊,采用增量的方式描述,细节可参考RFC7252 #3.1

option1

一般情况下Option部分包含Option Delta、Option Length和Option Value三部分。

​ 【Option Delta】表示Option的增量,当前的Option的具体编号等于之前所有Option Delta的总和。

​ 【Option Length】表示Option Value的具体长度。

​ 【Option Value】表示Option具体内容

​ CoAP中所有的Option都采用编号的方式,这些Option及编号的定义如下图所示。

option2

在这些option中,Uri-Host、Uri-Port、Uri-Path和Uri-Query等和资源“位置”和参数有关。

​ 【3】Uri-Host:CoAP主机名称,例如iot.eclipse.org

​ 【7】Uri-Port:CoAP端口号,默认为5683

​ 【11】Uri-Path:资源路由或路径,例如\temperature。资源路径采用UTF8字符串形式,长度不计第一个”\”。

​ 【15】Uri-Query:访问资源参数,例如?value1=1&value2=2,参数与参数之间使用“&”分隔,Uri-Query和Uri-Path之间采用“?”分隔。

​ 在这些option中,Content-Format和Accept用于表示CoAP负载的媒体格式

​ 【12】Content-Format:指定CoAP复杂媒体类型,媒体类型采用整数描述,例如application/json对应整数50,application/octet-stream对应整数40。

​ 【17】Accept: 指定CoAP响应复杂中的媒体类型,媒体类型的定义和Content-Format相同。

​ CoAP协议中支持多个Option,例如

​ 第一个Option Delta=11,表示该Option表示Uri-Path(11)

​ 第二个Option Delta=1,表示该Option=1+11,表示Content-Format(12)

​ 第三个Option Delta=3,表示该Option=3+1+11,表示Uri-Query(15)

​ CoAP采用这样的方式表示多个Option,而每种Option都可以在HTTP协议中找到对应项。

OpenFlow1.0.0 代码解读——add port

Posted on 2018-07-24

此文章解析主函数中没有详细解读的 add_ports 函数。

主函数引用调用如下:

1
2
3
if (port_list) {
add_ports(dp, port_list);
}

在函数开始时,将 port_list 里的端口打开。

add_port

strtok_r 函数相当于是 linux 平台下的 strtok 函数,主要用于分隔结构体的各成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void
add_ports(struct datapath *dp, char *port_list)
{
char *port, *save_ptr;
/* Glibc 2.7 has a bug in strtok_r when compiling with optimization that
* can cause segfaults here:
* http://sources.redhat.com/bugzilla/show_bug.cgi?id=5614.
* Using ",," instead of the obvious "," works around it. */
for (port = strtok_r(port_list, ",,", &save_ptr); port;
port = strtok_r(NULL, ",,", &save_ptr)) {
int error = dp_add_port(dp, port, num_queues);
if (error) {
ofp_fatal(error, "failed to add port %s", port);
}
}
}

函数首先对 port_list 结构体做处理,赋值到参数 port 中,port 作为具体的端口名,调用 dp_add_port 函数。

dp_add_port

此函数中的 port 与上一层函数的 port 不同,add_port 中的 port 是一个 char 型指针变量,指向端口名。这一函数中的 port 是 sw_port 结构体的指针,具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sw_port {
uint32_t config; /* Some subset of OFPPC_* flags. */
uint32_t state; /* Some subset of OFPPS_* flags. */
struct datapath *dp;
struct netdev *netdev;
struct list node; /* Element in datapath.ports. */
unsigned long long int rx_packets, tx_packets;
unsigned long long int rx_bytes, tx_bytes;
unsigned long long int tx_dropped;
uint16_t port_no;
/* port queues */
uint16_t num_queues;
struct sw_queue queues[NETDEV_MAX_QUEUES];
struct list queue_list; /* list of all queues for this port */
};

sw_port 结构体中包含具体的网络端口 netdev ,所以函数判断 port 结构体中的 netdev 是否为空,从而判断端口是否打开。没有打开则调用 new_port 函数打开端口。

1
2
3
4
5
6
7
8
9
10
11
12
int
dp_add_port(struct datapath *dp, const char *netdev, uint16_t num_queues)
{
int port_no;
for (port_no = 1; port_no < DP_MAX_PORTS; port_no++) {
struct sw_port *port = &dp->ports[port_no];
if (!port->netdev) {
return new_port(dp, port, port_no, netdev, NULL, num_queues);
}
}
return EXFULL;
}

new_port

该函数的主要功能包括:

  • 调用 netdev_open 函数打开网络设备,成功则返回0。具体的网络设备处理由 do_open_netdev 执行。
  • 设置网络设备的mac地址
  • 设置flags
  • 设置ip地址
  • 添加信息到 port 结构体中
  • 调用 send_port_status 将端口设置成功的消息发送到与controller之间的安全信道

函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
static int
new_port(struct datapath *dp, struct sw_port *port, uint16_t port_no,
const char *netdev_name, const uint8_t *new_mac, uint16_t num_queues)
{
struct netdev *netdev;
struct in6_addr in6;
struct in_addr in4;
int error;

error = netdev_open(netdev_name, NETDEV_ETH_TYPE_ANY, &netdev);
if (error) {
return error;
}
if (new_mac && !eth_addr_equals(netdev_get_etheraddr(netdev), new_mac)) {
/* Generally the device has to be down before we change its hardware
* address. Don't bother to check for an error because it's really
* the netdev_set_etheraddr() call below that we care about. */
netdev_set_flags(netdev, 0, false);
error = netdev_set_etheraddr(netdev, new_mac);
if (error) {
VLOG_WARN("failed to change %s Ethernet address "
"to "ETH_ADDR_FMT": %s",
netdev_name, ETH_ADDR_ARGS(new_mac), strerror(error));
}
}
error = netdev_set_flags(netdev, NETDEV_UP | NETDEV_PROMISC, false);
if (error) {
VLOG_ERR("failed to set promiscuous mode on %s device", netdev_name);
netdev_close(netdev);
return error;
}
if (netdev_get_in4(netdev, &in4)) {
VLOG_ERR("%s device has assigned IP address %s",
netdev_name, inet_ntoa(in4));
}
if (netdev_get_in6(netdev, &in6)) {
char in6_name[INET6_ADDRSTRLEN + 1];
inet_ntop(AF_INET6, &in6, in6_name, sizeof in6_name);
VLOG_ERR("%s device has assigned IPv6 address %s",
netdev_name, in6_name);
}

if (num_queues > 0) {
error = netdev_setup_slicing(netdev, num_queues);
if (error) {
VLOG_ERR("failed to configure slicing on %s device: "\
"check INSTALL for dependencies, or rerun "\
"using --no-slicing option to disable slicing",
netdev_name);
netdev_close(netdev);
return error;
}
}

memset(port, '\0', sizeof *port);

list_init(&port->queue_list);
port->dp = dp;
port->netdev = netdev;
port->port_no = port_no;
port->num_queues = num_queues;
list_push_back(&dp->port_list, &port->node);

/* Notify the ctlpath that this port has been added */
send_port_status(port, OFPPR_ADD);

return 0;
}

可以重点看一下 do_open_netdev 和 send_port_status 函数。

do_open_netdev

此函数是以 netdev_open 函数作为入口被调用的,可以简单看一下 netdev_open 函数。

netdev_open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Opens the network device named 'name' (e.g. "eth0") and returns zero if
* successful, otherwise a positive errno value. On success, sets '*netdevp'
* to the new network device, otherwise to null.
*
* 'ethertype' may be a 16-bit Ethernet protocol value in host byte order to
* capture frames of that type received on the device. It may also be one of
* the 'enum netdev_pseudo_ethertype' values to receive frames in one of those
* categories. */
int
netdev_open(const char *name, int ethertype, struct netdev **netdevp)
{
if (!strncmp(name, "tap:", 4)) {
return netdev_open_tap(name + 4, netdevp);
} else {
return do_open_netdev(name, ethertype, -1, netdevp);
}
}

总体比较简单,需要特地说明的是输入参数 ethertype 。该参数在 netdev_open 中的输入是 NETDEV_ETH_TYPE_ANY 。在 netdev.h 中有具体定义:

1
2
3
4
5
enum netdev_pseudo_ethertype {
NETDEV_ETH_TYPE_NONE = -128, /* Receive no frames. */
NETDEV_ETH_TYPE_ANY, /* Receive all frames. */
NETDEV_ETH_TYPE_802_2 /* Receive all IEEE 802.2 frames. */
};

回到 do_open_netdev 函数,该函数代码如下,不过多解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
static int
do_open_netdev(const char *name, int ethertype, int tap_fd,
struct netdev **netdev_)
{
int netdev_fd;
struct sockaddr_ll sll;
struct ifreq ifr;
unsigned int ifindex;
uint8_t etheraddr[ETH_ADDR_LEN];
struct in6_addr in6;
int mtu;
int txqlen;
int hwaddr_family;
int error;
struct netdev *netdev;

init_netdev();
*netdev_ = NULL;

/* Create raw socket. */
netdev_fd = socket(PF_PACKET, SOCK_RAW,
htons(ethertype == NETDEV_ETH_TYPE_NONE ? 0
: ethertype == NETDEV_ETH_TYPE_ANY ? ETH_P_ALL
: ethertype == NETDEV_ETH_TYPE_802_2 ? ETH_P_802_2
: ethertype));
if (netdev_fd < 0) {
return errno;
}

/* Set non-blocking mode. */
error = set_nonblocking(netdev_fd);
if (error) {
goto error_already_set;
}

/* Get ethernet device index. */
strncpy(ifr.ifr_name, name, sizeof ifr.ifr_name);
if (ioctl(netdev_fd, SIOCGIFINDEX, &ifr) < 0) {
VLOG_ERR("ioctl(SIOCGIFINDEX) on %s device failed: %s",
name, strerror(errno));
goto error;
}
ifindex = ifr.ifr_ifindex;

/* Bind to specific ethernet device. */
memset(&sll, 0, sizeof sll);
sll.sll_family = AF_PACKET;
sll.sll_ifindex = ifindex;
if (bind(netdev_fd, (struct sockaddr *) &sll, sizeof sll) < 0) {
VLOG_ERR("bind to %s failed: %s", name, strerror(errno));
goto error;
}

if (ethertype != NETDEV_ETH_TYPE_NONE) {
/* Between the socket() and bind() calls above, the socket receives all
* packets of the requested type on all system interfaces. We do not
* want to receive that data, but there is no way to avoid it. So we
* must now drain out the receive queue. */
error = drain_rcvbuf(netdev_fd);
if (error) {
goto error;
}
}

/* Get MAC address. */
if (ioctl(netdev_fd, SIOCGIFHWADDR, &ifr) < 0) {
VLOG_ERR("ioctl(SIOCGIFHWADDR) on %s device failed: %s",
name, strerror(errno));
goto error;
}
hwaddr_family = ifr.ifr_hwaddr.sa_family;
if (hwaddr_family != AF_UNSPEC && hwaddr_family != ARPHRD_ETHER) {
VLOG_WARN("%s device has unknown hardware address family %d",
name, hwaddr_family);
}
memcpy(etheraddr, ifr.ifr_hwaddr.sa_data, sizeof etheraddr);

/* Get MTU. */
if (ioctl(netdev_fd, SIOCGIFMTU, &ifr) < 0) {
VLOG_ERR("ioctl(SIOCGIFMTU) on %s device failed: %s",
name, strerror(errno));
goto error;
}
mtu = ifr.ifr_mtu;

/* Get TX queue length. */
if (ioctl(netdev_fd, SIOCGIFTXQLEN, &ifr) < 0) {
VLOG_ERR("ioctl(SIOCGIFTXQLEN) on %s device failed: %s",
name, strerror(errno));
goto error;
}
txqlen = ifr.ifr_qlen;

get_ipv6_address(name, &in6);

/* Allocate network device. */
netdev = xmalloc(sizeof *netdev);
netdev->name = xstrdup(name);
netdev->ifindex = ifindex;
netdev->txqlen = txqlen;
netdev->hwaddr_family = hwaddr_family;
netdev->netdev_fd = netdev_fd;
netdev->tap_fd = tap_fd < 0 ? netdev_fd : tap_fd;
netdev->queue_fd[0] = netdev->tap_fd;
memcpy(netdev->etheraddr, etheraddr, sizeof etheraddr);
netdev->mtu = mtu;
netdev->in6 = in6;
netdev->num_queues = 0;

/* Get speed, features. */
do_ethtool(netdev);

/* Save flags to restore at close or exit. */
error = get_flags(netdev->name, &netdev->save_flags);
if (error) {
goto error_already_set;
}
netdev->changed_flags = 0;
fatal_signal_block();
list_push_back(&netdev_list, &netdev->node);
fatal_signal_unblock();

/* Success! */
*netdev_ = netdev;
return 0;

error:
error = errno;
error_already_set:
close(netdev_fd);
if (tap_fd >= 0) {
close(tap_fd);
}
return error;
}

OpenFlow1.0.0 带无线扩展的OF1.0协议代码详解———main

Posted on 2018-07-24

openflow1.0.0 是斯坦福团队在2008年为满足AP在SDN系统中适配提出的openflow协议无线域扩展。此协议以openflow1.0为基础,解析处理 ieee80211 数据;利用 click 元素实现AP到控制器的转发功能。

这一篇文章从主函数出发,解读了代码的几个框架性函数。

主程序运行在 udatapath.c 中。

从 main 函数开始解读>
1
2
3
4
5
6
7
8
9
10
11
12
int
main(int argc, char *argv[])
{
int n_listeners;
int error;
int i;
set_program_name(argv[0]); //设置程序名
register_fault_handlers(); //注册新号故障处理器
time_init();
vlog_init();
parse_options(argc, argv); //解析选项
signal(SIGPIPE, SIG_IGN);

主要看一下 parse_options 函数,此函数定义了该子程序的解析选项。

parse_options

这里不是完整的函数代码,我们主要关注参数选项和对应的执行函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
  static void
parse_options(int argc, char *argv[])
{
...
static struct option long_options[] = {
{"interfaces", required_argument, 0, 'i'},
{"local-port", required_argument, 0, 'L'},
{"no-local-port", no_argument, 0, OPT_NO_LOCAL_PORT},
{"datapath-id", required_argument, 0, 'd'},
{"verbose", optional_argument, 0, 'v'},
{"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'},
{"no-slicing", no_argument, 0, OPT_NO_SLICING},
{"mfr-desc", required_argument, 0, OPT_MFR_DESC},
{"hw-desc", required_argument, 0, OPT_HW_DESC},
{"sw-desc", required_argument, 0, OPT_SW_DESC},
{"dp_desc", required_argument, 0, OPT_DP_DESC},
{"serial_num", required_argument, 0, OPT_SERIAL_NUM},
DAEMON_LONG_OPTIONS,

for (;;) {
int indexptr;
int c;

c = getopt_long(argc, argv, short_options, long_options, &indexptr);
if (c == -1) {
break;
}

switch (c) {
case 'd':
if (strlen(optarg) != 12
|| strspn(optarg, "0123456789abcdefABCDEF") != 12) {
ofp_fatal(0, "argument to -d or --datapath-id must be "
"exactly 12 hex digits");
}
dpid = strtoll(optarg, NULL, 16);
if (!dpid) {
ofp_fatal(0, "argument to -d or --datapath-id must "
"be nonzero");
}
break;

case 'h':
usage();

case 'V':
printf("%s %s compiled "__DATE__" "__TIME__"\n",
program_name, VERSION BUILDNR);
exit(EXIT_SUCCESS);

case 'v':
vlog_set_verbosity(optarg);
break;

case 'i':
if (!port_list) {
port_list = optarg;
} else {
port_list = xasprintf("%s,%s", port_list, optarg);
}
break;

case 'L':
local_port = optarg;
break;

case OPT_NO_LOCAL_PORT:
local_port = NULL;
break;

case OPT_MFR_DESC:
strncpy(mfr_desc, optarg, sizeof mfr_desc);
break;

case OPT_HW_DESC:
strncpy(hw_desc, optarg, sizeof hw_desc);
break;

case OPT_SW_DESC:
strncpy(sw_desc, optarg, sizeof sw_desc);
break;

case OPT_DP_DESC:
strncpy(dp_desc, optarg, sizeof dp_desc);
break;

case OPT_SERIAL_NUM:
strncpy(serial_num, optarg, sizeof serial_num);
break;

case OPT_NO_SLICING:
num_queues = 0;
break;

DAEMON_OPTION_HANDLERS

}
}
free(short_options);
}

主函数对程序输入参数进行排错判断

1
2
3
4
if (argc - optind < 1) {
ofp_fatal(0, "at least one listener argument is required; "
"use --help for usage");
}

建立新的datapath

1
error = dp_new(&dp, dpid);

查看 dp_new 函数

dp_new

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int
dp_new(struct datapath **dp_, uint64_t dpid)
{
struct datapath *dp;

dp = calloc(1, sizeof *dp);
if (!dp) {
return ENOMEM;
}

dp->last_timeout = time_now();
list_init(&dp->remotes);
dp->listeners = NULL;
dp->n_listeners = 0;
dp->id = dpid <= UINT64_C(0xffffffffffff) ? dpid : gen_datapath_id();
dp->chain = chain_create(dp); //Creates and returns a new chain.
if (!dp->chain) {
VLOG_ERR("could not create chain");
free(dp);
return ENOMEM;
}

list_init(&dp->port_list);
dp->flags = 0;
dp->miss_send_len = OFP_DEFAULT_MISS_SEND_LEN;

if(strlen(&dp_desc) > 0) /* use the comment, if specified */
strncpy(dp->dp_desc, &dp_desc, sizeof dp->dp_desc);
else /* else, just use "$HOSTNAME pid=$$" */
{
char hostnametmp[DESC_STR_LEN];
gethostname(hostnametmp,sizeof hostnametmp);
snprintf(dp->dp_desc, sizeof dp->dp_desc,"%s pid=%u",hostnametmp, getpid());
}

*dp_ = dp;
return 0;
}

此函数对 datapath 结构体的各成员做出初始化定义,其中比较重要的是 chain ,一起来看一下 chain_create 结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Creates and returns a new chain.  Returns NULL if the chain cannot be
* created. */
struct sw_chain *chain_create(struct datapath *dp)
{
struct sw_chain *chain = calloc(1, sizeof *chain); //分配内存空间
if (chain == NULL)
return NULL;

chain->dp = dp;
if (add_table(chain, table_hash2_create(0x1EDC6F41, TABLE_HASH_MAX_FLOWS,
0x741B8CD7, TABLE_HASH_MAX_FLOWS),
0)
|| add_table(chain, table_linear_create(TABLE_LINEAR_MAX_FLOWS), 0)
|| add_table(chain, table_linear_create(TABLE_LINEAR_MAX_FLOWS), 1)) {
chain_destroy(chain);
return NULL;
}

return chain;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* Attempts to append 'table' to the set of tables in 'chain'.  Returns 0 or
* negative error. If 'table' is null it is assumed that table creation failed
* due to out-of-memory. */
static int add_table(struct sw_chain *chain, struct sw_table *table, int emerg)
{
if (table == NULL)
return -ENOMEM;
if (chain->n_tables >= CHAIN_MAX_TABLES) {
VLOG_ERR("too many tables in chain\n");
table->destroy(table);
return -ENOBUFS;
}
if (emerg)
chain->emerg_table = table;
else
chain->tables[chain->n_tables++] = table;
return 0;
}

回到 main 函数,这一段代码主要实现虚拟通道的建立和连接,不做详解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
n_listeners = 0;
for (i = optind; i < argc; i++) {
const char *pvconn_name = argv[i];
struct pvconn *pvconn;
int retval;

retval = pvconn_open(pvconn_name, &pvconn);
if (!retval || retval == EAGAIN) {
dp_add_pvconn(dp, pvconn);
n_listeners++;
} else {
ofp_error(retval, "opening %s", pvconn_name);
}
}
if (!n_listeners) {
ofp_fatal(0, "could not listen for any connections");
}

if (port_list) {
add_ports(dp, port_list);
}
if (local_port) {
error = dp_add_local_port(dp, local_port, 0);
if (error) {
ofp_fatal(error, "failed to add local port %s", local_port);
}
}

error = vlog_server_listen(NULL, NULL);
if (error) {
ofp_fatal(error, "could not listen for vlog connections");
}

如果相同程序以存在则停止此程序,运行守护进程:

1
2
die_if_already_running();
daemonize();

daemonize(void)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void
daemonize(void)
{
if (detach) { //detach是一个bool值,判断是否作为后台程序运行
char c = 0;
int fds[2];
if (pipe(fds) < 0) {
ofp_fatal(errno, "pipe failed");
}

switch (fork()) {
default:
/* Parent process: wait for child to create pidfile, then exit. */
close(fds[1]);
fatal_signal_fork();
if (read(fds[0], &c, 1) != 1) {
ofp_fatal(errno, "daemon child failed to signal startup");
}
exit(0);

case 0:
/* Child process. */
close(fds[0]);
make_pidfile(); //进程文件
write(fds[1], &c, 1); //读写操作
close(fds[1]);
setsid();
chdir("/");
break;

case -1:
/* Error. */
ofp_fatal(errno, "could not fork");
break;
}
} else {
make_pidfile();
}
}

接下来,主函数建立socket通道。前面提到该AP由click模式实现与控制器或上层 openflow 交换机的交互,click相当于一个交换数据的交换机,用不同端口将数据交换隔离开。openflow1.0.0 程序建立 socket 通道,将数据包以 udp 协议传输到click模块中。通道由 make_socket 函数建立。

1
2
3
4
/*add the monitor function*/
tp = realloc(tp, sizeof(struct thread_para));
tp->dp = dp;
make_socket(tp);

make_socket

thread_para 是一个包含端口port和datapath的结构体,具体定义为:

1
2
3
4
struct thread_para {
struct border_port *bp;
struct datapath *dp;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//server socket using UDP
int make_socket(struct thread_para *tp) {

printf("enter make socket function\n");
const int SERV_PORT = 5555;
pthread_t tid;
int sockfd;
struct sockaddr_in servaddr;

bzero(&servaddr , sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);

if((sockfd = socket(AF_INET , SOCK_DGRAM , 0)) < 0)
{
perror("socket error");
exit(1);
}
//_fd = sockfd;
struct border_port* bp;
bp = realloc(bp, sizeof(struct border_port));
tp->bp = bp;
tp->bp->fd = sockfd;

if(bind(sockfd , (struct sockaddr *)&servaddr , sizeof(servaddr)))
{
perror("bind error");
exit(1);
}

printf("create server socket\n");
//create thread to send and receive message from click module
if( pthread_create(&tid, NULL, socket_handler, tp) != 0 )
pdie("pthread_create");

return 0;
}

后台进程建立成功后,主函数中设计了一个死循环,运行 dp_run, dp_wait, poll_back 三个函数。

1
2
3
4
5
for (;;) {
dp_run(dp);
dp_wait(dp);
poll_block();
}

我们主要来看一下 dp_run 函数,其余的 dp_wait 和 poll_block 函数不做详解。

dp_run

此函数顾名思义就是运行一个datapath,其中主要重要的函数包括 netdev_recv 、fwd_port_input 和 remote_run ,将会在后面具体分析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
void
dp_run(struct datapath *dp)
{
time_t now = time_now();
struct sw_port *p, *pn;
struct remote *r, *rn;
struct ofpbuf *buffer = NULL;
size_t i;

if (now != dp->last_timeout) {
struct list deleted = LIST_INITIALIZER(&deleted);
struct sw_flow *f, *n;

chain_timeout(dp->chain, &deleted);
LIST_FOR_EACH_SAFE (f, n, struct sw_flow, node, &deleted) {
dp_send_flow_end(dp, f, f->reason);
list_remove(&f->node);
flow_free(f);
}
dp->last_timeout = now;
}
poll_timer_wait(1000);

/*
* 以上对超时做了排错处理
* dp_send_flow_end 将错误代码发送到控制器,具体分析见下。
*/

LIST_FOR_EACH_SAFE (p, pn, struct sw_port, node, &dp->port_list) {
int error;

if (!buffer) {
/* Allocate buffer with some headroom to add headers in forwarding
* to the controller or adding a vlan tag, plus an extra 2 bytes to
* allow IP headers to be aligned on a 4-byte boundary. */
const int headroom = 128 + 2;
const int hard_header = VLAN_ETH_HEADER_LEN;
const int mtu = netdev_get_mtu(p->netdev);
buffer = ofpbuf_new(headroom + hard_header + mtu);
buffer->data = (char*)buffer->data + headroom;
}

/*
* buffer用来暂时存储netdevice的数据包
* ofbuf_new 定义了一个空的buffer空间
*/
error = netdev_recv(p->netdev, buffer);
if (!error) {
p->rx_packets++;
p->rx_bytes += buffer->size;
fwd_port_input(dp, buffer, p);
buffer = NULL;
} else if (error != EAGAIN) {
VLOG_ERR_RL(&rl, "error receiving data from %s: %s",
netdev_get_name(p->netdev), strerror(error));
}
}
ofpbuf_delete(buffer);

/*
* 建立与Controller之间的安全信道
*/

/* Talk to remotes. */
LIST_FOR_EACH_SAFE (r, rn, struct remote, node, &dp->remotes) {
remote_run(dp, r);
}

for (i = 0; i < dp->n_listeners; ) {
struct pvconn *pvconn = dp->listeners[i];
struct vconn *new_vconn;
int retval = pvconn_accept(pvconn, OFP_VERSION, &new_vconn);
if (!retval) {
remote_create(dp, rconn_new_from_vconn("passive", new_vconn));
} else if (retval != EAGAIN) {
VLOG_WARN_RL(&rl, "accept failed (%s)", strerror(retval));
dp->listeners[i] = dp->listeners[--dp->n_listeners];
continue;
}
i++;
}
}

ofpbuf_new 提供一个空的内存空间。

1
2
3
4
5
6
7
struct ofpbuf *
ofpbuf_new(size_t size)
{
struct ofpbuf *b = xmalloc(sizeof *b);
ofpbuf_init(b, size);
return b;
}

netdev_recv

netdev_recv 将网络设备接受到的数据包存进缓存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int
netdev_recv(struct netdev *netdev, struct ofpbuf *buffer)
{
ssize_t n_bytes;
struct sockaddr_ll sll;
socklen_t sll_len;

assert(buffer->size == 0);
assert(ofpbuf_tailroom(buffer) >= ETH_TOTAL_MIN);

/* prepare to call recvfrom */
memset(&sll,0,sizeof sll);
sll_len = sizeof sll;

/* cannot execute recvfrom over a tap device */
if (!strncmp(netdev->name, "tap", 3)) {
do {
n_bytes = read(netdev->tap_fd, ofpbuf_tail(buffer),
(ssize_t)ofpbuf_tailroom(buffer));
} while (n_bytes < 0 && errno == EINTR);
}
else {
do {
n_bytes = recvfrom(netdev->tap_fd, ofpbuf_tail(buffer),
(ssize_t)ofpbuf_tailroom(buffer), 0,
(struct sockaddr *)&sll, &sll_len);
} while (n_bytes < 0 && errno == EINTR);
}
if (n_bytes < 0) {
if (errno != EAGAIN) {
VLOG_WARN_RL(&rl, "error receiving Ethernet packet on %s: %s",
strerror(errno), netdev->name);
}
return errno;
} else {
/* we have multiple raw sockets at the same interface, so we also
* receive what others send, and need to filter them out.
* TODO(yiannisy): can we install this as a BPF at kernel? */
if (sll.sll_pkttype == PACKET_OUTGOING) {
return EAGAIN;
}


buffer->size += n_bytes;

/* When the kernel internally sends out an Ethernet frame on an
* interface, it gives us a copy *before* padding the frame to the
* minimum length. Thus, when it sends out something like an ARP
* request, we see a too-short frame. So pad it out to the minimum
* length. */
pad_to_minimum_length(buffer);
return 0;
}
}

此函数利用 recvfrom 函数将网络设备收到的数据存储在 buffer 的 ofpbuf_tail 中。接受到数据包之后, fwd_port_input 函数首先对其处理。

fwd_port_input

1
2
3
4
5
6
7
8
9
10
11
/* 'buffer' was received on 'p', which may be a a physical switch port or a
* null pointer. Process it according to 'dp''s flow table, sending it up to
* the controller if no flow matches. Takes ownership of 'buffer'. */
void fwd_port_input(struct datapath *dp, struct ofpbuf *buffer,
struct sw_port *p)
{
if (run_flow_through_tables(dp, buffer, p)) {
dp_output_control(dp, buffer, p->port_no,
dp->miss_send_len, OFPR_NO_MATCH);
}
}

数据包首先由 run_flow_through_tables 判断是否需要输出至控制器,再由 dp_output_control 函数操作。

run_flow_through_tables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 'buffer' was received on 'p', which may be a a physical switch port or a
* null pointer. Process it according to 'dp''s flow table. Returns 0 if
* successful, in which case 'buffer' is destroyed, or -ESRCH if there is no
* matching flow, in which case 'buffer' still belongs to the caller. */
int run_flow_through_tables(struct datapath *dp, struct ofpbuf *buffer,
struct sw_port *p)
{
struct sw_flow_key key;
struct sw_flow *flow;

key.wildcards = 0;
if (flow_extract(buffer, p ? p->port_no : OFPP_NONE, &key.flow)
&& (dp->flags & OFPC_FRAG_MASK) == OFPC_FRAG_DROP) {
/* Drop fragment. */
ofpbuf_delete(buffer);
return 0;
}

if (p && p->config & (OFPPC_NO_RECV | OFPPC_NO_RECV_STP)
&& p->config & (!eth_addr_equals(key.flow.dl_dst, stp_eth_addr)
? OFPPC_NO_RECV : OFPPC_NO_RECV_STP)) {
ofpbuf_delete(buffer);
return 0;
}

flow = chain_lookup(dp->chain, &key, 0);
if (flow != NULL) {
flow_used(flow, buffer);
execute_actions(dp, buffer, &key, flow->sf_acts->actions,
flow->sf_acts->actions_len, false);
return 0;
} else {
return -ESRCH;
}
}

首先,flow_extract 解析数据包,该函数主要判断数据包是否为IP数据包,是则返回1;否则返回0。如果是IP数据包,程序将删除这一缓存,不进行下一步处理。同时,函数将数据包中的各信息位信息存储到结构体 flow 中。flow 由 sw_flow 定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct sw_flow {
struct sw_flow_key key;

uint64_t cookie; /* Opaque controller-issued identifier. */
uint16_t priority; /* Only used on entries with wildcards. */
uint16_t idle_timeout; /* Idle time before discarding (seconds). */
uint16_t hard_timeout; /* Hard expiration time (seconds) */
uint64_t used; /* Last used time. */
uint64_t created; /* When the flow was created. */
uint64_t packet_count; /* Number of packets seen. */
uint64_t byte_count; /* Number of bytes seen. */
uint8_t reason; /* Reason flow removed (one of OFPRR_*). */
uint8_t send_flow_rem; /* Send a flow removed to the controller */
uint8_t emerg_flow; /* Emergency flow indicator */

struct sw_flow_actions *sf_acts;

/* Private to table implementations. */
struct list node;
struct list iter_node;
unsigned long int serial;
};

数据流解析函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/* Returns 1 if 'packet' is an IP fragment, 0 otherwise. */
int
flow_extract(struct ofpbuf *packet, uint16_t in_port, struct flow *flow)
{
struct ofpbuf b = *packet;
struct eth_header *eth;
int retval = 0;

memset(flow, 0, sizeof *flow);
flow->dl_vlan = htons(OFP_VLAN_NONE);
flow->in_port = htons(in_port);

packet->l2 = b.data;
packet->l3 = NULL;
packet->l4 = NULL;
packet->l7 = NULL;

eth = pull_eth(&b);
if (eth) {
if (ntohs(eth->eth_type) >= OFP_DL_TYPE_ETH2_CUTOFF) {
/* This is an Ethernet II frame */
flow->dl_type = eth->eth_type;
} else {
/* This is an 802.2 frame */
struct llc_header *llc = ofpbuf_at(&b, 0, sizeof *llc);
struct snap_header *snap = ofpbuf_at(&b, sizeof *llc,
sizeof *snap);
if (llc == NULL) {
return 0;
}
if (snap
&& llc->llc_dsap == LLC_DSAP_SNAP
&& llc->llc_ssap == LLC_SSAP_SNAP
&& llc->llc_cntl == LLC_CNTL_SNAP
&& !memcmp(snap->snap_org, SNAP_ORG_ETHERNET,
sizeof snap->snap_org)) {
flow->dl_type = snap->snap_type;
ofpbuf_pull(&b, LLC_SNAP_HEADER_LEN);
} else {
flow->dl_type = htons(OFP_DL_TYPE_NOT_ETH_TYPE);
ofpbuf_pull(&b, sizeof(struct llc_header));
}
}

/* Check for a VLAN tag */
if (flow->dl_type == htons(ETH_TYPE_VLAN)) {
struct vlan_header *vh = pull_vlan(&b);
if (vh) {
flow->dl_type = vh->vlan_next_type;
flow->dl_vlan = vh->vlan_tci & htons(VLAN_VID_MASK);
flow->dl_vlan_pcp = (uint8_t)((ntohs(vh->vlan_tci) >> VLAN_PCP_SHIFT)
& VLAN_PCP_BITMASK);
}
}
memcpy(flow->dl_src, eth->eth_src, ETH_ADDR_LEN);
memcpy(flow->dl_dst, eth->eth_dst, ETH_ADDR_LEN);

packet->l3 = b.data;
if (flow->dl_type == htons(ETH_TYPE_IP)) {
const struct ip_header *nh = pull_ip(&b);
if (nh) {
flow->nw_tos = nh->ip_tos & 0xfc;
flow->nw_proto = nh->ip_proto;
flow->nw_src = nh->ip_src;
flow->nw_dst = nh->ip_dst;
packet->l4 = b.data;
if (!IP_IS_FRAGMENT(nh->ip_frag_off)) {
if (flow->nw_proto == IP_TYPE_TCP) {
const struct tcp_header *tcp = pull_tcp(&b);
if (tcp) {
flow->tp_src = tcp->tcp_src;
flow->tp_dst = tcp->tcp_dst;
packet->l7 = b.data;
} else {
/* Avoid tricking other code into thinking that
* this packet has an L4 header. */
flow->nw_proto = 0;
}
} else if (flow->nw_proto == IP_TYPE_UDP) {
const struct udp_header *udp = pull_udp(&b);
if (udp) {
flow->tp_src = udp->udp_src;
flow->tp_dst = udp->udp_dst;
packet->l7 = b.data;
} else {
/* Avoid tricking other code into thinking that
* this packet has an L4 header. */
flow->nw_proto = 0;
}
} else if (flow->nw_proto == IP_TYPE_ICMP) {
const struct icmp_header *icmp = pull_icmp(&b);
if (icmp) {
flow->icmp_type = htons(icmp->icmp_type);
flow->icmp_code = htons(icmp->icmp_code);
packet->l7 = b.data;
} else {
/* Avoid tricking other code into thinking that
* this packet has an L4 header. */
flow->nw_proto = 0;
}
}
} else {
retval = 1;
}
}
} else if (flow->dl_type == htons(ETH_TYPE_ARP)) {
const struct arp_eth_header *arp = pull_arp(&b);
if (arp) {
if (arp->ar_pro == htons(ARP_PRO_IP) && arp->ar_pln == IP_ADDR_LEN) {
flow->nw_src = arp->ar_spa;
flow->nw_dst = arp->ar_tpa;
}
flow->nw_proto = ntohs(arp->ar_op) && 0xff;
}
}
}
return retval;
}

如果不是一个IP数据,数据包的信息现已存储在结构体 flow 中。

接着,通过 chain_lookup 函数为此数据包匹配对应的 key 并存入 flow 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Searches 'chain' for a flow matching 'key', which must not have any wildcard
* fields. Returns the flow if successful, otherwise a null pointer. */
struct sw_flow *
chain_lookup(struct sw_chain *chain, const struct sw_flow_key *key, int emerg)
{
int i;

assert(!key->wildcards);

if (emerg) {
struct sw_table *t = chain->emerg_table;
struct sw_flow *flow = t->lookup(t, key);
t->n_lookup++;
if (flow) {
t->n_matched++;
return flow;
}
} else {
for (i = 0; i < chain->n_tables; i++) {
struct sw_table *t = chain->tables[i];
struct sw_flow *flow = t->lookup(t, key);
t->n_lookup++;
if (flow) {
t->n_matched++;
return flow;
}
}
}

return NULL;
}

最后, execute_actions 函数执行该flow的 action 。

execute_actions

此函数需要的参数包括 datapath,buffer,key,acitons,actions_len。

由函数的调用可以看到,actions 和 actions_len 存储在flow的 sw_flow_actions 结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/* Execute a list of actions against 'buffer'. */
void execute_actions(struct datapath *dp, struct ofpbuf *buffer,
struct sw_flow_key *key,
const struct ofp_action_header *actions, size_t actions_len,
int ignore_no_fwd)
{
/* Every output action needs a separate clone of 'buffer', but the common
* case is just a single output action, so that doing a clone and then
* freeing the original buffer is wasteful. So the following code is
* slightly obscure just to avoid that. */
int prev_port;
uint32_t prev_queue;
size_t max_len = UINT16_MAX;
uint16_t in_port = ntohs(key->flow.in_port);
uint8_t *p = (uint8_t *)actions;

prev_port = -1;
prev_queue = 0;

/* The action list was already validated, so we can be a bit looser
* in our sanity-checking. */
while (actions_len > 0) {
struct ofp_action_header *ah = (struct ofp_action_header *)p;
size_t len = htons(ah->len);

if (prev_port != -1) {
do_output(dp, ofpbuf_clone(buffer), in_port, max_len,
prev_port, prev_queue, ignore_no_fwd);
prev_port = -1;
}

if (ah->type == htons(OFPAT_OUTPUT)) {
struct ofp_action_output *oa = (struct ofp_action_output *)p;
prev_port = ntohs(oa->port);
prev_queue = 0; /* using the default best-effort queue */
max_len = ntohs(oa->max_len);
} else if (ah->type == htons(OFPAT_ENQUEUE)) {
struct ofp_action_enqueue *ea = (struct ofp_action_enqueue *)p;
prev_port = ntohs(ea->port);
prev_queue = ntohl(ea->queue_id);
max_len = 0; /* we will not send to the controller anyways - useless */
} else {
uint16_t type = ntohs(ah->type);

if (type < ARRAY_SIZE(of_actions)) {
execute_ofpat(buffer, key, ah, type);
} else if (type == OFPAT_VENDOR) {
execute_vendor(buffer, key, ah);
}
}

p += len;
actions_len -= len;
}
if (prev_port != -1) {
do_output(dp, buffer, in_port, max_len, prev_port, prev_queue, ignore_no_fwd);
} else {
ofpbuf_delete(buffer);
}
}

此函数确定转发前后的端口,对 OpenFlow 协议的 build-in action 和 vendor aciton 分别执行 execute_ofpact 和 execute_vendor ,其他 actions 统一由 do_output 执行。

do_output

函数首先根据输出端口判断该数据包是转发给控制器的还是由端口输出的。

1
2
3
4
5
6
7
8
9
10
11
static void
do_output(struct datapath *dp, struct ofpbuf *buffer, int in_port,
size_t max_len, int out_port, uint32_t queue_id,
bool ignore_no_fwd)
{
if (out_port != OFPP_CONTROLLER) {
dp_output_port(dp, buffer, in_port, out_port, queue_id, ignore_no_fwd);
} else {
dp_output_control(dp, buffer, in_port, max_len, OFPR_ACTION);
}
}
do_output_port

根据不同的 out_port 对数据包进行处理,涉及的处理函数有 output_packet ,output_all 和 dp_output_control 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** Takes ownership of 'buffer' and transmits it to 'out_port' on 'dp'.
*/
void
dp_output_port(struct datapath *dp, struct ofpbuf *buffer,
int in_port, int out_port, uint32_t queue_id,
bool ignore_no_fwd UNUSED)
{

assert(buffer);
switch (out_port) {
case OFPP_IN_PORT:
output_packet(dp, buffer, in_port, queue_id);
break;

case OFPP_TABLE: {
struct sw_port *p = dp_lookup_port(dp, in_port);
if (run_flow_through_tables(dp, buffer, p)) {
ofpbuf_delete(buffer);
}
break;
}

case OFPP_FLOOD:
output_all(dp, buffer, in_port, 1);
break;

case OFPP_ALL:
output_all(dp, buffer, in_port, 0);
break;

case OFPP_CONTROLLER:
dp_output_control(dp, buffer, in_port, UINT16_MAX, OFPR_ACTION);
break;

case OFPP_LOCAL:
default:
if (in_port == out_port) {
VLOG_DBG_RL(&rl, "can't directly forward to input port");
return;
}
output_packet(dp, buffer, out_port, queue_id);
break;
}
}
  • output_packet 函数利用 netdev_send 函数将数据包由网络设备发出。
  • output_all 函数将数据包从所有端口发出,在函数内调用 do_output_port ,每次发送后将端口号加1继续发送。
do_output_control
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Takes ownership of 'buffer' and transmits it to 'dp''s controller.  If the
* packet can be saved in a buffer, then only the first max_len bytes of
* 'buffer' are sent; otherwise, all of 'buffer' is sent. 'reason' indicates
* why 'buffer' is being sent. 'max_len' sets the maximum number of bytes that
* the caller wants to be sent. */
void
dp_output_control(struct datapath *dp, struct ofpbuf *buffer, int in_port,
size_t max_len, int reason)
{
struct ofp_packet_in *opi;
size_t total_len;
uint32_t buffer_id;

buffer_id = save_buffer(buffer);
total_len = buffer->size;
if (buffer_id != UINT32_MAX && buffer->size > max_len) {
buffer->size = max_len;
}

opi = ofpbuf_push_uninit(buffer, offsetof(struct ofp_packet_in, data));
opi->header.version = OFP_VERSION;
opi->header.type = OFPT_PACKET_IN;
opi->header.length = htons(buffer->size);
opi->header.xid = htonl(0);
opi->buffer_id = htonl(buffer_id);
opi->total_len = htons(total_len);
opi->in_port = htons(in_port);
opi->reason = reason;
opi->pad = 0;
send_openflow_buffer(dp, buffer, NULL);
}

函数利用 ofpbuf_push_uninit 函数重构 buffer 的数据包头部,然后用 send_openflow_buffer 函数将 buffer 发出。

1
2
3
4
5
6
7
8
void *
ofpbuf_push_uninit(struct ofpbuf *b, size_t size)
{
ofpbuf_prealloc_headroom(b, size);
b->data = (char*)b->data - size;
b->size += size;
return b->data;
}

在解析 send_openflow_buffer 函数之前,先来看一下之前提到的 remote_run 函数。

remote_run

此函数主要通过构建虚拟连接来建立一个远程进程,我的个人理解是通过这个函数来建立AP与Controller之间的安全信道。

首先,结构体 remote 构建了一个安全信道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* A connection to a secure channel. */
struct remote {
struct list node;
struct rconn *rconn;
#define TXQ_LIMIT 128 /* Max number of packets to queue for tx. */
int n_txq; /* Number of packets queued for tx on rconn. */

/* Support for reliable, multi-message replies to requests.
*
* If an incoming request needs to have a reliable reply that might
* require multiple messages, it can use remote_start_dump() to set up
* a callback that will be called as buffer space for replies. */
int (*cb_dump)(struct datapath *, void *aux);
void (*cb_done)(void *aux);
void *cb_aux;
};

结构体 rconn 被定义为一个连接控制器或交换机的可靠连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/* A reliable connection to an OpenFlow switch or controller.
*
* See the large comment in rconn.h for more information. */
struct rconn {
enum state state;
time_t state_entered;

struct vconn *vconn;
char *name;
bool reliable;

struct ofp_queue txq;

int backoff;
int max_backoff;
time_t backoff_deadline;
time_t last_received;
time_t last_connected;
unsigned int packets_sent;
unsigned int seqno;

/* In S_ACTIVE and S_IDLE, probably_admitted reports whether we believe
* that the peer has made a (positive) admission control decision on our
* connection. If we have not yet been (probably) admitted, then the
* connection does not reset the timer used for deciding whether the switch
* should go into fail-open mode.
*
* last_admitted reports the last time we believe such a positive admission
* control decision was made. */
bool probably_admitted;
time_t last_admitted;

/* These values are simply for statistics reporting, not used directly by
* anything internal to the rconn (or the secchan for that matter). */
unsigned int packets_received;
unsigned int n_attempted_connections, n_successful_connections;
time_t creation_time;
unsigned long int total_time_connected;

/* If we can't connect to the peer, it could be for any number of reasons.
* Usually, one would assume it is because the peer is not running or
* because the network is partitioned. But it could also be because the
* network topology has changed, in which case the upper layer will need to
* reassess it (in particular, obtain a new IP address via DHCP and find
* the new location of the controller). We set this flag when we suspect
* that this could be the case. */
bool questionable_connectivity;
time_t last_questioned;

/* Throughout this file, "probe" is shorthand for "inactivity probe".
* When nothing has been received from the peer for a while, we send out
* an echo request as an inactivity probe packet. We should receive back
* a response. */
int probe_interval; /* Secs of inactivity before sending probe. */

/* Messages sent or received are copied to the monitor connections. */
#define MAX_MONITORS 8
struct vconn *monitors[8];
size_t n_monitors;

/* Protocol statistical informaition. */
struct ofpstat ofps_rcvd;
struct ofpstat ofps_sent;

uint32_t idle_echo_xid;
};

这是一个虚拟的连接,具体连接又由 vconn-provider 提供,包括状态、版本、IP以及接收和发送的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Active virtual connection to an OpenFlow device.
*
* This structure should be treated as opaque by vconn implementations. */
struct vconn {
struct vconn_class *class;
int state;
int error;
int min_version;
int version;
uint32_t ip;
char *name;
bool reconnectable;
struct ofpstat ofps_rcvd;
struct ofpstat ofps_sent;
};

所以,总而言之可以把 remote 视为AP的安全信道。

在解析remote_run函数前,还要说明一个结构体:sender。sender由remote结构体和一个id组成,用以表示接收到的 OpenFlow 消息。

1
2
3
4
5
/* The origin of a received OpenFlow message, to enable sending a reply. */
struct sender {
struct remote *remote; /* The device that sent the message. */
uint32_t xid; /* The OpenFlow transaction ID. */
};

remote_run 函数主要职责是保持安全信道运行并捕获由controller发来的消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
static void
remote_run(struct datapath *dp, struct remote *r)
{
int i;

rconn_run(r->rconn);

/* Do some remote processing, but cap it at a reasonable amount so that
* other processing doesn't starve. */
for (i = 0; i < 50; i++) {
if (!r->cb_dump) {
struct ofpbuf *buffer;
struct ofp_header *oh;

buffer = rconn_recv(r->rconn);
if (!buffer) {
break;
}

if (buffer->size >= sizeof *oh) {
struct sender sender;

oh = (struct ofp_header *)buffer->data;
sender.remote = r;
sender.xid = oh->xid;
fwd_control_input(dp, &sender, buffer->data, buffer->size);
} else {
VLOG_WARN_RL(&rl, "received too-short OpenFlow message");
}
ofpbuf_delete(buffer);
} else {
if (r->n_txq < TXQ_LIMIT) {
int error = r->cb_dump(dp, r->cb_aux);
if (error <= 0) {
if (error) {
VLOG_WARN_RL(&rl, "dump callback error: %s",
strerror(-error));
}
r->cb_done(r->cb_aux);
r->cb_dump = NULL;
}
} else {
break;
}
}
}

if (!rconn_is_alive(r->rconn)) {
remote_destroy(r);
}
}
  • rconn_run 函数建立连接。
  • rconn_recv 接受连接中的数据包,并将数据包存储在 buffer 中。
  • fwd_control_input 解析接收到的数据包并进行下一步操作。

fwd_control_input

  • 从数据包的包头中提取数据,根据不同数据类型进行操作。
  • 操作函数暂存在 handler 中。
  • 具体的操作函数将在后续文章中逐个介绍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/* 'msg', which is 'length' bytes long, was received from the control path.
* Apply it to 'chain'. */
int
fwd_control_input(struct datapath *dp, const struct sender *sender,
const void *msg, size_t length)
{
int (*handler)(struct datapath *, const struct sender *, const void *);
struct ofp_header *oh;
size_t min_size;

/* Check encapsulated length. */
oh = (struct ofp_header *) msg;
if (ntohs(oh->length) > length) {
return -EINVAL;
}
assert(oh->version == OFP_VERSION);

/* Figure out how to handle it. */
switch (oh->type) {
case OFPT_BARRIER_REQUEST:
min_size = sizeof(struct ofp_header);
handler = recv_barrier_request;
break;
case OFPT_FEATURES_REQUEST:
min_size = sizeof(struct ofp_header);
handler = recv_features_request;
break;
case OFPT_GET_CONFIG_REQUEST:
min_size = sizeof(struct ofp_header);
handler = recv_get_config_request;
break;
case OFPT_SET_CONFIG:
min_size = sizeof(struct ofp_switch_config);
handler = recv_set_config;
break;
case OFPT_PACKET_OUT:
min_size = sizeof(struct ofp_packet_out);
handler = recv_packet_out;
break;
case OFPT_FLOW_MOD:
min_size = sizeof(struct ofp_flow_mod);
handler = recv_flow;
break;
case OFPT_PORT_MOD:
min_size = sizeof(struct ofp_port_mod);
handler = recv_port_mod;
break;
case OFPT_STATS_REQUEST:
min_size = sizeof(struct ofp_stats_request);
handler = recv_stats_request;
break;
case OFPT_ECHO_REQUEST:
min_size = sizeof(struct ofp_header);
handler = recv_echo_request;
break;
case OFPT_ECHO_REPLY:
min_size = sizeof(struct ofp_header);
handler = recv_echo_reply;
break;
case OFPT_QUEUE_GET_CONFIG_REQUEST:
min_size = sizeof(struct ofp_header);
handler = recv_queue_get_config_request;
break;
case OFPT_VENDOR:
min_size = sizeof(struct ofp_vendor_header);
handler = recv_vendor;
break;
default:
dp_send_error_msg(dp, sender, OFPET_BAD_REQUEST, OFPBRC_BAD_TYPE,
msg, length);
return -EINVAL;
}

/* Handle it. */
if (length < min_size)
return -EFAULT;
return handler(dp, sender, msg);
}

最后回到非常重要的函数 send_openflow_buffer ,此函数也在之前多次看到,现在我们来看一下它的具体代码。

send_openflow_buffer

  • 函数首先判断是否为控制器发送来的数据,如果是,则sender不为零,执行第一个if操作,发送回去。
  • 如果不是,解析目的端口,然后用 send_openflow_buffer_to_remote 把 buffer 发送到安全信道。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int
send_openflow_buffer(struct datapath *dp, struct ofpbuf *buffer,
const struct sender *sender)
{
update_openflow_length(buffer);
if (sender) {
/* Send back to the sender. */
return send_openflow_buffer_to_remote(buffer, sender->remote);
} else {
/* Broadcast to all remotes. */
struct remote *r, *prev = NULL;
LIST_FOR_EACH (r, struct remote, node, &dp->remotes) {
if (prev) {
//send_openflow_buffer_to_remote(ofpbuf_clone(buffer), prev);
send_openflow_buffer_to_remote(buffer, prev);
}
prev = r;
}
if (prev) {
send_openflow_buffer_to_remote(buffer, prev);
} else {
ofpbuf_delete(buffer);
}
return 0;
}
}
send_openflow_buffer_to_remote
1
2
3
4
5
6
7
8
9
10
11
static int
send_openflow_buffer_to_remote(struct ofpbuf *buffer, struct remote *remote)
{
int retval = rconn_send_with_limit(remote->rconn, buffer, &remote->n_txq,
TXQ_LIMIT);
if (retval) {
VLOG_WARN_RL(&rl, "send to %s failed: %s",
rconn_get_name(remote->rconn), strerror(retval));
}
return retval;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Sends 'b' on 'rc'.  Increments '*n_queued' while the packet is in flight; it
* will be decremented when it has been sent (or discarded due to
* disconnection). Returns 0 if successful, EAGAIN if '*n_queued' is already
* at least as large as 'queue_limit', or ENOTCONN if 'rc' is not currently
* connected. Regardless of return value, 'b' is destroyed.
*
* Because 'b' may be sent (or discarded) before this function returns, the
* caller may not be able to observe any change in '*n_queued'.
*
* There is no rconn_send_wait() function: an rconn has a send queue that it
* takes care of sending if you call rconn_run(), which will have the side
* effect of waking up poll_block(). */
int
rconn_send_with_limit(struct rconn *rc, struct ofpbuf *b,
int *n_queued, int queue_limit)
{
int retval;
retval = *n_queued >= queue_limit ? EAGAIN : rconn_send(rc, b, n_queued);
if (retval) {
ofpbuf_delete(b);
}
return retval;
}

这几段代码比较简单,不过多说明,总之就是通过 rconn_send 发送到安全信道中。

因为是主函数直接调用的函数,总体还是框架性的函数。其他功能性函数将在接下里的文章中介绍。

Open VSwitch in OpenWrt

Posted on 2018-06-16

在这一篇博客中,我将详细介绍如何在OpenWrt系统中搭建Open VSwitch,从而让AP适用于SDN系统。

AP有“瘦AP”(Fit AP)和“胖AP”(Fat AP)之分,Fat AP是与Fit AP相对来讲的,Fat AP将WLAN的物理层、用户数据加密认证、QoS、网络管理、漫游技术以及其他应用层的功能集于一身。Fat AP无线网络解决方案可由由Fat AP直接在有线网的基础上构成,设备结构复杂,且难于集中管理,比较常用于家庭无线接入,即所谓的“无线路由器”。Fit AP是一个只有加密、射频功能的AP,功能单一,不能独立工作。整个Fit AP无线网络解决方案由无线控制器和Fit AP在有线网的基础上构成。Fit AP上“零配置”,所有配置都集中到无线控制器上。这也促成了Fit AP解决方案更加便于集中管理,并由此具有三层漫游、基于用户下发权限等Fat AP不具备的功能。

本文的解决方案为Fat AP的SDN适配方案,解决思路为Fat AP + Openwrt + OVS。我将介绍三种在Openwrt下安装OVS的方法。

Install OpenVSwitch in OpenWrt

方法一 编译一个带Open VSwitch版本的OpenWrt固件

这种方法是在Linux系统下直接编译一个带有Open VSwitch的固件,并将固件刷入AP中。

此方法操作步骤简单,但容易出错,编译过程常伴随各种未知错误。本人尝试过多次,编译过多种版本下的固件,失败多过成功。但这种方法还是推荐大家学习,方便日后进一步学习扩展。

  • 升级软件安装包,安装编译时需要的组件。

    1
    2
    sudo apt-get update
    sudo apt-get install gccg++binutilspatchbzip2flexbison make autoconf gettext texinfo unzip sharutils subversion libncurses5-devncurses-term zlib1g-dev subversion git gawk asciidoc libz-dev
  • 获取OpenWrt源码并添加OVS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    git clone git://git.openwrt.org/openwrt.git #获取Openwrt源码
    cd openwrt
    ./scripts/feeds update –a #更新最新代码
    ./scripts/feeds install –a #安装最新组件
    svn up #更新版本号
    #添加openwrt代码连接
    echo 'src-git openvswitch git://github.com/pichuang/openvwrt.git' >> feeds.conf
    #或者(建立代码连接)
    echo 'src-git openvswitch git://github.com/ttsubo/openvswitch.git' >> feeds.conf

    ./scripts/feeds update openvswitch
    ./scripts/feeds install -a -p openvswitch
  • 编译固件

    1
    make menuconfig
    • 1 选择CPU型号(根据AP硬件选择,可参见openwrt的wiki页面)

      1
      Target System—–Atheros AR71xx/AR7240/AR913x/AR934x
    • 2 选择路由型号(根据路由器型号选择)

      1
      TargetProfile—-xxxx
    • 3 添加 Luci 及相关组件(语言包+DNNS)

      1
      2
      3
      LuCI—>Collections—–<*> luci
      LuCI—>Translations—-<*> luci-i18n-chinese
      LuCI—>Applications —> <*>luci-app-ddns.
    • 4 添加UTF8编码

      1
      Kernel modules —> Native Language Support —> <*> kmod-nls-utf8
    • 5 添加复位键支持

      1
      Utilities —> <*> restorefactory
    • 6 添加 openvswitch

      1
      Network -> openvswitch-switch, openvswitch-switch, openvswitch-ipsec (Optional)
    • 7 save

      1
      make V=99

固件编译成功后将在 target 文件夹中找到 .bin 文件,然后将镜像文件刷进系统,就得到了一个有Open VSwitch的AP。

方法二 通过opkg安装Open Vswitch

这种方法是利用Openwrt系统本身的opkg(Open PacKaGe management)管理器直接从互联网下载OVS的安装文件和相关依赖。

此方法有个前提是必须保证AP本身可以上网。

####AP连入互联网

Openwrt系统的官方固件中的WAN是默认DPCH上网的,最简单的上网方式是连接到上级网络的路由器:用网线连接此AP的WAN口和可上网的路由器的LAN口。这样Openwrt可以直接通过DPCH方式上网,不需要任何配置。

如果没有一个正常上网的路由器,AP联网的方式和其他商用路由器类似。首先将AP的WAN口连接到墙上的网口,打开luci界面:192.168.1.1,选择接口(interfaces),编辑WAN,然后按照运营商的上网方式设置相对的上网协议,输入账号密码等。

安装步骤

  • 打开SecureCRT用SSH协议连接到Openwrt(ip地址:192.168.1.1,具体方式可参见上一篇博客)。

  • 更新安装包并安装

    1
    2
    3
    opkg update -a
    opkg install kmod-openvswitch
    opkg install openvswitch

我在使用这个方法安装时,一直出现无法连接到官方下载网站的问题。无法完成更新,也无法安装。

方法三 使用ipk安装包安装

第三种方法是我自己发现的方法,优点是简单而且不容易出错,但缺点就是太麻烦。如果走投无路了,也可以尝试一下这个方法。

此方法就是在Openwrt的软件库上下载相对应的安装包,放入Openwrt的 /tmp/文件夹内,利用opkg install命令安装。

  • 在openwrt的archive网站找到对应的Openwrt固件版本

    attitude_adjustment v12.09

    backfire v10.03 / v10.03.1

    barrier_breaker v14.07

    chaos_calmer v15.05 / v15.05.1

    releases v17.01.0 - v17.01.4

    snapshots trunk

  • 进入分类和版本内,选择相应文件系统,普遍是ar71xx。

  • 选择 genetic/文件夹

  • 选择Supplementary Files中的packages。

  • packages文件夹中有六个文件夹,所有的ipk文件都在这六个文件夹中。

    ​ base/ Openwrt系统工具(大部分工具安装包可在其中找到)

    ​ luci/ luci相关

    ​ management/ 管理包(不常用)

    ​ packages/ 工具和其他依赖文件

    ​ routing/ 不常用

    ​ telephony/ 不常用

    大部分的基础工具都可以在 base/ 文件夹中找到,所需的依赖文件可在 packages/ 中找到。

  • 在packages/文件夹中,用ctrl+F搜索 openvswitch ,下载以下ipk文件:

    ​ openvswitch_xx.xx.xx (xx是版本号)

    ​ openvswitch-benchmark_xx.xx.xx

    ​ kmod_openvswitch_xx.xx.xx

  • 打开SecureCRT软件和WinSCP软件,连接到AP。

  • 用WinSCP将ipk文件导入到 Openwrt 系统的 /tmp/ 文件夹下。

  • 在SecureCRT中输入

    1
    2
    cd /tmp/
    opkg install openvswitch_xx.xx.xx.ipk #正确文件名
  • 安装一定会失败,并提示缺少以下依赖,此时,在archive文件夹中根据错误提示逐个寻找依赖的ipk文件包,用同样的方式进行安装。

  • 安装完所有缺省再次安装openvswitch的ipk。

  • 安装ovs时,如果出现内核版本不对的错误提示,但又确实是安装了同一个CPU版本下的ovs包,可以尝试强制安装,如果安装之后可以使用ovs,就没有问题。

    1
    opkg --force-depends install openvswitch_xx.xx.xx.ipk

这个方法确实非常麻烦,但是成功率很高,同时也适用于安装其他类型的工具包,所以比较推荐。


以上是我总结的三种在openwrt上安装openvswitch的方法,下一篇我讲继续介绍ovs的相关配置和使用。

Start With OpenWrt /3

Posted on 2018-05-29

Start with OpenWrt /3

终极刷机——TTL法


TTL刷机就是指用TTL线(串口线)让路由器与电脑的BOOT进行串口通信,然后通过网口利用TFTP把固件传输过来。

所以这种方法同样适合在路由器死机变砖了之后救活它。

引用一个滑冰犀鸟的原理解释:

原理说明

  • TTL线就是串口线(USB转串口)。
  • 系统固件不是通过TTL线传输到路由器中。
  • TTL线的作用是让PC端和BOOT进行串口通信。
  • PC端通过串口线使用BOOT的控制台。
  • 系统固件实际是通过网口(TFTP协议)传输到路由器中。

准备工作

  • TTL刷机需要进行一些焊接工作,提前准备好电烙铁,排针,杜邦线等电工工具。

  • TTL转USB线,可以在淘宝买到,但是一定要买对型号,具体可参考wiki或询问淘宝客服。

  • 网线

  • Windows上安装SecureCRT、Putty、Tftpd(注意选择32位/64位)。

硬件准备

首先,在OpenWrt的wiki页面找到相应型号路由器的电路板图片。以TP-LINK 841n为例,它的wiki页面下有一个Serial port settings:栏,在其中你可以找到电路板上GND RX TX在哪(一般都不会使用到VCC,但具体还是要根据路由器型号判断)。

hardware

然后,将GND口、TX和RX焊上排针,再用杜邦线连接出来。

TTL转USB线比较推荐如图这样的,无须再焊接,把从排针印出来的杜邦线接上就可以了。需要注意的是,购买转串口线的时候一定要注意型号,在wiki看好型号再去找,买的时候也要多和卖家沟通,否则会出现扫码率怎么都对不上等乱七八糟的问题。

杜邦线

接着,将电路板引出来的TX接到串口线的RX,RX接到TX,GND接GND,电路方面的准备工作理论上就完成了。

正式刷机

  • 设置电脑ip设置到与路由器相同网段,如:192.168.1.169
  • 打开SecureCRT,按照如下设置并连接。

​ protol: Serial

​ port: COMx (在电脑的设备管理器中查看端口号)

​ Baud rate: 115200(通常是这个速率,但具体值根据型号不同有所不同)

​ Data bits: 8

​ Parity: None

​ Stop bits: 1

​ Flow Control: 均不钩选

​

  • 将电脑的无线网卡禁用,如果电脑有多张有线网卡也将其他几张禁用,关闭防火墙。

  • 把官方固件(openwrt-ar71xx-generic-tl-wr841nd-v7-squashfs-factory.bin

    )与Tftpd32放在同一个文件夹中(官方固件的获取方式在上两篇文章中有介绍),打开tftpd。

  • 将USB串口连接到电脑,为路由器通电,WAN口用网线与电脑网口相连,在SecureCRT中可以看到开机信息,则证明串口连接成功。

    如果出现乱码,应该是Baud rate设置有误;如果没有反应,但路由器灯正常亮起,则可能是电路出了问题。

  • 将路由器断电,重新通电,在SecureCRT上查看打印信息,出现

    Autobooting in 1s…

    马上在键盘上按下tpl(也有一些路由器需要按Ctrl+c 或者其他,具体可以参照特定路由器的刷机教程)

  • 成功进入路由器系统的boot界面。

  • 在窗口输入:

    1
    2
    setenv ipaddr 192.168.1.1
    setenv serverip 192.168.1.169

    ​ ipaddr 是路由器的ip地址

    ​ serverip是电脑的ip地址

  • 将固件拷到路由器内存中,0x80000000是flash上一段空白位置,可以暂时存储文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    ar7240> tftpboot 0x80000000 openwrt-ar71xx-generic-tl-wr841n-v8-squashfs-factory.bin
    Using eth0 device
    TFTP from server 192.168.1.100; our IP address is 192.168.1.111
    Filename 'openwrt-ar71xx-generic-tl-wr841n-v8-squashfs-factory.bin'.
    Load address: 0x80000000
    Loading: checksum bad
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    #################################################################
    ######################################################
    done
    Bytes transferred = 3932160 (3c0000 hex)
  • 将flash原来的固件文件擦除

    1
    ar7240> erase 0x9f020000 +0x3c0000

    0x9f020000为内核的启动地址,在开机的引导信息中可以看到,见840N的U-boot行

    Booting image at 9f020000 …

    不同的路由器启动地址也有所不同,具体情况可以在uboot行中看到。

    0x3c0000为固件大小,这个输错了路由器会变砖,上一步返回信息的最后一句会给出。

  • 擦除完成后,将openwrt固件复制到固件启动地址中,bootm启动。

    1
    2
    ar7240> cp.b 0x80000000 0x9f020000 0x3c0000
    ar7240> bootm 0x9f020000
  • 之后,系统就开始重新启动了,将计算机网线接入LAN口,完成刷机啦。

总结

特别说明,这里的刷机过程是以TP-LINK TL-WR841N为例的,其他型号可能在细节上需要稍作改动,但具体都可以参考Openwrt wiki,当然,这句话也是经常出现在我的教程里,就是去wiki页面参考。虽然wiki是英文版面,有时候大家看起来会有点吃力而遗漏很多细节,但是其中真的包罗万象,几乎能解决你所有遇到的问题。所以在wiki中好好探索吧。

虽然想写一个适用于新手的教程,但是写下来才发现,其实每个人遇到的问题都是不一样的,而我也只能主要强调我踩过的坑。所以也许也不是那么适合所有人,这一点真的非常遗憾。

Start With OpenWrt /2

Posted on 2018-05-13

Start With OpenWrt /2

刷机进阶——TFTP法

这两种方式在Openwrt的wiki页面应该都有很详细的指导教程,我的经验是,如果你找到了一个自认为靠谱的教程,也记得先看看wiki上的介绍,有个大致的概念和对错判断。如果两者有出入,基本还是以wiki为准的。


TFTP

用TFTP方式刷机的原理其实和网页版本差不多,有一些版本的路由器甚至要挂载TFTP服务器才能完成固件更新。我的理解来说,TFTP的好处就在于有时候可能可以跨过网页版运营商对非官方固件的屏蔽,比较粗暴得导入固件。

用TFTP的方式,个人推荐是在Linux系统下进行,命令行输入、启动tftp服务器什么的会更方便。

准备工作

  • 1.安装并启动TFTP服务器(TFTP Server),注意将TFTP固件放在home(host)文件夹下,因为用tftp传输时,固件前不能添加路径。
1
2
sudo apt-get install tftpd-hpa tftp
sudo cp ~/uboot/arch/arm/boot/uboot.img /var/lib/tftpboot
  • 2.将固件重命名为简单形式,如 a1.bin。(这一步可有可无,仅仅为了方便接下来的命令行输入)
  • 3.TFTP传输过程中必须保证网络链接在路由器开启电源时就建立,为保重这一步成功,可以先关闭网络。(根据你linux系统的版本从以下选择合适的命令行)
1
2
3
4
5
6
/etc/init.d/networking stop
/etc/init.d/network stop
/etc/init.d/NetworkManager stop
service networking stop
service network stop
service NetworkManager stop
  • 4.配置与引导程序相匹配的静态IP地址。(一般为192.168.1.1,可以从路由器wiki页面中找到相关信息)
1
ifconfig eth0 ipv4.x.y.z netmask 255.255.255.0
  • 5.为TFTP预置arp请求条目
1
2
3
arp -s ipv4.x.y.1 20:aa:bb:cc:dd:00
#ipv4.x.y.1 是路由器ip地址
#20:aa:bb:cc:dd:00 是路由器背面的MAC地址

基本步骤

做完以上准备工作可以正式开始了。用TFTP客户端(TFTP Client)将固件上传路由器的基本方法如下:

  • 1.断开路由器电源
  • 2.用网线将电脑与路由器LAN口相连
  • 3.在电脑上启动TFTP客户端

  • 4.TFTP到路由器的ip地址(可在wiki页面查询)

1
2
tftp IPv4.x.y.z
#IPv4.x.y.z 为路由器的ip地址
  • 5.设置TFTP传输模式为octet/binary(二进制传输)
1
2
tftp> binary 
tftp> rexmt 1 #传输间隔为1秒
  • 6.设置TFTP重传直至成功
1
2
3
tftp> timeout 60
tftp> trace
tftp> Packet tracing on.
  • 7.用put命令设置传输的固件
1
tftp> put openwrt-xxx-x.x-xxx.bin
  • 8.TFTP客户端开始工作的同时打开路由器电源
  • 9.路由器大概需要十多秒才会成功启动,再次之前TFTP客户端持续发送请求。检测到路由器后,TFTP客户端将开始发送固件。
  • 10.等待传输完成,路由器重启。

TFTP方式的其他命令行形式实现

aTFTP

  • 一条命令行
1
atftp --trace --option "timeout 1" --option "mode octet" --put --local-file openwrt-xxx-x.x-xxx.bin IPv4.x.y.z

​ 按下回车的同时接通路由器电源

  • 分布命令行
1
2
3
4
5
6
atftp
connect IPv4.x.y.z
mode octet
trace
timeout 1
put openwrt-xxx-x.x-xxx.bin

Netkit’s TFTP

  • 一条命令行
1
echo -e "binary\nrexmt 1\ntimeout 60\ntrace\nput openwrt-xxx-x.x-xxx.bin\n" | tftp IPv4.x.y.z
  • 分布命令行
1
2
3
4
5
6
7
tftp IPv4.x.y.z
tftp> binary
tftp> rexmt 1
tftp> timeout 60
tftp> trace
tftp> Packet tracing on.
tftp> put openwrt-xxx-x.x-xxx.bin

curl

1
curl -T openwrt-xxx-x.x-xxx.bin tftp://IPv4.x.y.z

Tips总结

用TFTP的方式有几个需要特别注意的点:

  • 一定要将固件放在home文件夹下,因为tftp put 的时候只添加文件名,不能前置文件路径。
  • 开始tftp传输的同时接通电源
  • 主机的ip地址要与路由器的ip地址在一个子网下,所以不要忘记提前把网络模式从DHCP改成Static。

希望这篇能够帮助到你,用tftp方式刷机总体来说实现难度不大,但是一定要细心,不要遗漏任何步骤。遇到问题可以详见OpenWrt的Wiki页面,基本都有详细的教程。同时也欢迎通过Email联系我。

Start With OpenWrt

Posted on 2018-05-12

Start With OpenWrt

从挑选路由器到刷机

不管你的用途是什么,只要涉及自定义路由器,Openwrt是最常用也最好用的开源系统。把一款商用路由器刷成Openwrt路由器,其实就是更改路由器的固件——将路由器默认的固件(.bin)文件替换。那么如何将市面上买的商用路由器刷成Openwrt系统呢?第一步要做的,是先选择一款路由器。


选择路由器

首先,选择路由器基本可以遵循以下步骤:

  • 选择一个路由器品牌(Brand)(D-Link,TP-Link等),在网上查询现有售卖的型号(Model)。
  • 在Openwrt的wiki页面上选择品牌和型号,与市面上还能买到的路由器对应查看,选择一款能够买到的。
  • 进入对应路由器wiki页面,查看Openwrt官方固件出的版本号(Version)。
  • 向卖家确认路由器版本号。

到这里都没有碰壁,只能说你非常幸运。因为通过正规渠道有售的路由器在Openwrt上能找到的就已经不多,其次即使找到了相同的型号,市面上的路由器版本也往往太新,而Openwrt没有release新版本的固件。

这里需要特别注意的是,旧版本一般不能适配新版本,虽然他们的型号一样,但是不同版本之间的硬件设置不同。而且,同一个版本的海内外产品也有区别。

还要说明的是,OpenWrt是国外的平台,而路由器厂商针对中国市场和海外市场对同一款路由器有不同版本的配置。一般而言,海外版本的路由器更适合搭载Openwrt系统,因为他们往往拥有更大的Flash内存。国内的新路由器一般只配置4M的Flash内存(有的甚至只有2M),基本只够勉强塞入固件系统(3.8M),更别提承载其他配置。

所以我的经验是,通过正规渠道一般已经没有办法购买到合适的路由器了,就算真的购买到了,后续可能也需要对路由器的Flash和RAM进行扩容。嫌麻烦的人其实可以直接上淘宝买个改装好的路由器,有工程追求的人请参见以下几篇硬改教程:

  • aggresss的专栏
  • TP-LINK 845N V1硬改
  • TP-LINK_841N_V8路由器 (好文!)

路由器刷机

现在,你有了一个可以刷Openwrt官方固件的路由器,但是我们默认路由器内置的固件还是运营商提供的。所以这一步讲的是如何将固件刷成Openwrt,难度从低到高有三种:

  • 网页法
  • TFTP法
  • TTL法

在开始刷机之前,要先对路由器的结构有个基本的认识。路由器上的网线接口一般有1个WAN口,和多个LAN口,一般有不同的颜色和标识。

路由器网口标识

除此之外,还有一个电源接口和一个小小的reset孔(一般而言reset孔在里面,要用牙签或者针头戳进去)。

网页法

首先在路由器的Openwrt Wiki 页面里找到官方的release版本,下载到本地。注意区分Install Image和Update Image,这里我们选择Install Image,文件后缀是squashfs-factory.bin。

下载镜像和更新镜像

然后,将路由器断电,将LAN1口用网线与电脑连接,然后摁住reset按钮,同时接通电源。

观察路由器的亮灯情况,一般要维持10s左右,亮灯状态维持稳定时,松开reset按钮。因为不同的路由器亮灯情况不同,如果不放心可以直接搜索 路由器型号+刷机 ,找到教程看具体的状态。

从路由器背面查看ip地址,一般为:192.168.1.1,从网页输入ip地址登录。(账号密码信息也会在路由器背面贴出,一般默认是 admin ,如果没有可以上路由器官网查看。)

进入页面后,选择软件升级,然后添加刚刚下载的 .bin 文件,点击更新即可。注意过程中保持电源通电,等待路由器重启成功。

通过网页的方式非常简单,但是也经常失败,如果不成功可以尝试:

  • 关闭电脑防火墙
  • 查看版本号是否对应

路由器运营商也可能会针对网页刷机做一些屏蔽,所以网页刷机往往成功率不高。如果失败了,可以继续尝试用TFTP方式甚至TTL方式刷机。具体方法,请参加以下两篇。

TFTP法

TTL法

Fantin

Fantin

9 posts
5 tags
© 2019 Fantin
Powered by Hexo
Theme - NexT.Muse