NBNS客户端的C语言实现
接上一篇【网络协议详解1 - NBNS】对NBNS的介绍,这一篇将要讲述使用C语言如何实现一个NBNS客户端,用于向局域网内其它设备发送NBNS NODE STATUS QUERY,并将接收到的RESPONSE信息打印出来。其实也就是一个UDP socket的实例。
编写一个客户端,首先要清楚它要完成什么任务,进而确定完成任务的方法和步骤。其任务很简单,就是NBNS数据包的发送和接收,也就是一个简单的socket收发进程。实现主要分以下几步:
- 组包得到NBNS NODE STATUS QUERY
- 发送NBNS NODE STATUS QUERY
- 接收RESPONSE并解析
include & define
首先来看看编写该客户端会用到的头文件及相关宏定义
#include <stdio.h>
#include <stdlib.h> /* for exit */
#include <string.h> /* for memset */
#include <unistd.h> /* for close */
#include <sys/socket.h> /* for socket, bind */
#include <sys/time.h> /* for timeval */
#include <netinet/in.h> /* for socksaddr_in */
#include <arpa/inet.h> /* for htons */
#define NBNS_PORT 137
#define NBNS_TYPE_NB 0x0020
#define NBNS_TYPE_NBSTAT 0x0021
#define NBNS_CLASS_IN 0x0001
#define BUFFER_SIZE 1024 /* buffer size for recv packet */
#define IP_SIZE 16 /* length of IP address like 192.168.1.1 */
关于NBNS,其公知的端口号是137,在发送请求时会在目的地址中用到。
组包结构体
根据上一篇的内容,我们可以知道NBNS NODE STATUS的请求包和响应包的数据格式,据此可以编写相应的结构体。
typedef unsigned int uint32;
typedef unsigned short uint16;
typedef unsigned char uint8;
struct nbns_header {
uint16 tid; /* Transaction ID */
uint16 flags;
uint16 question;
uint16 answer;
uint16 authority;
uint16 additional;
} __attribute__ ((packed));
// NBNS NODE STATUS QUERY FORMAT
struct nbns_request {
struct nbns_header header;
char qname[34];
uint16 qtype;
uint16 qclass;
} __attribute__ ((packed));
struct nbns_name {
char name[16];
uint16 flags;
} __attribute__ ((packed));
// NBNS NODE STATUS RESPONSE FORMAT
struct nbns_response_header {
struct nbns_header header;
char rname[34];
uint16 rtype;
uint16 rclass;
uint32 ttl;
uint16 length;
uint8 num_of_names;
} __attribute__ ((packed));
注意每个结构体后的__attribute__ ((packed))
,这是为了告诉编译器不要对其进行对齐优化,保持数据的紧凑性,以防数据解析出错。
名称编解码
在进入正题之前,我们先来看下NetBIOS名称的编解码,这在后续发送请求和解析响应中都会用到。从RFC1001可知,NBNS请求包中的Name是被编码的,在上一篇中已经讲述过了。
编码
对于NODE STATUS QUERY, 通常将NBNS name设为*
, 就是一个通配符,使用下面的函数可以对其编码。
// encode "*" to "CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
void encode_name(char *name)
{
int i;
name[0] = 0x20; /* length of name */
name[1] = (('*' >> 4) & 0x0F) + 'A';
name[2] = ('*' & 0x0F) + 'A';
for (i = 3; i < 33; i++)
name[i] = 'A';
name[i] = '\0'; /* end of name */
}
解码
解码也很简单,就是编码的逆过程。上面的编码函数只针对*
,但这里的解码函数却是针对所有满足格式要求的字符串。
void decode_name(char *name, char *dst)
{
int i;
if (name[0] != 0x20)
return;
for (i = 1; i < 0x20; i+=2)
dst[i/2] = (((name[i]-'A') << 4) & 0xF0) + ((name[i+1]-'A') & 0x0F);
dst[i/2] = '\0';
}
create socket
socket
好了,进入正题。在执行数据收发之前需要创建UDP socket, 这里会用到socket函数。
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
RETURN VALUE
On success, a file descriptor for the new socket is returned.
On error, -1 is returned, and errno is set appropriately.
下面简要介绍下相关参数。
domain
常见的网络通信域domain主要有AF_UNIX
,AF_INET
,AF_INET6
,AF_NETLINK
,对于常见的TCP/UDP传输,基于IPv4协议,我们选择AF_INET
即可。type
type代表socket类型,常用的包含针对TCP的SOCK_STREAM
、针对UDP的SOCK_DGRAM
,以及针对需要访问源网络数据(应用层以下数据)的SOCK_RAW
。显然本文使用的是SOCK_DGRAM
.protocol
protocol通常设为0,当然也可以针对协议进行配置,比如UDP对应的IPPROTO_UDP
,其中IPPROTO是IP Protocol的缩写。
创建socket
下面来实现一个open_socket函数,用于打开一个socket,可以把它当做一个信封,等待后面写好信件放进去。
int open_socket(void)
{
int sock;
struct sockaddr_in saddr;
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sock < 0)
handle_error("socket");
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = 0;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock, (struct sockaddr *)&saddr, sizeof(saddr)) == -1)
handle_error("bind");
return sock;
}
这里要注意几点:
- 网络字节序和主机字节序不一定一样,需要使用
htonl
,htons
函数进行转换- h - host
- n - network
- s - short 2个字节
- l - long 4个字节
- 源地址的端口port设为0,代表由网络协议栈分配
- bind函数用于绑定发送地址,类似于往信封上写好寄信人的地址信息
- handle_error是一个异常处理函数,检测到错误后调用其打印错误信息并退出程序。
发送请求
sendto
发送函数包括组包和发送两部分,将请求信息按照组包格式逐一赋值,然后通过sendto
发送数据包。
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
RETURN VALUE
On success, these calls return the number of bytes sent.
On error, -1 is returned, and errno is set appropriately.
发送成功则返回真实的发送字节数,否则返回-1并更新errno.
发送数据
int send_nbns_query(int sock, struct in_addr dest_ip)
{
struct sockaddr_in dest;
struct nbns_request req;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(NBNS_PORT);
dest.sin_addr = dest_ip;
req.header.tid = htons(1);
req.header.flags = htons(0x0000);
req.header.question = htons(1);
req.header.answer = 0;
req.header.authority = 0;
req.header.additional = 0;
encode_name(req.qname);
req.qtype = htons(NBNS_TYPE_NBSTAT);
req.qclass = htons(NBNS_CLASS_IN);
return sendto(sock, (char *)&req, sizeof(req), 0,
(struct sockaddr *)&dest, sizeof(dest));
}
注意,除了0、单字节数据、单字节数组外,其余数据都需要使用htons
或htonl
进行转换,将主机字节序转换为网络字节序。
接收响应
recvfrom
发送请求后,便可以使用recvfrom
函数接收响应数据了。
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
RETURN VALUE
These calls return the number of bytes received, or -1 if an error occurred.
In the event of an error, errno is set to indicate the error.
Datagram sockets in various domains permit zero-length datagrams.
When such a datagram is received, the return value is 0.
接收成功返回接收到的字节数,否则返回-1。当然如何接收字节为0则返回0.
接收数据
void recv_nbns_response(int sock)
{
struct timeval tv;
struct sockaddr_in src_addr;
socklen_t src_len = sizeof(src_addr);
ssize_t recv_len = 0;
char res_buf[BUFFER_SIZE];
char src_ip[IP_SIZE] = {0};
tv.tv_sec = 5;
tv.tv_usec = 0;
/* set timeout 5s for some not exist PC */
if (setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0)
handle_error("setsockopt");
recv_len = recvfrom(sock, (char *)res_buf, BUFFER_SIZE, 0,
(struct sockaddr *)&src_addr, &src_len);
if (recv_len < 0)
handle_error("recvfrom");
else if (recv_len < sizeof(struct nbns_response_header))
return;
inet_ntop(AF_INET, (struct in_addr *)&src_addr.sin_addr,
(char *)&src_ip, IP_SIZE);
printf("recv response from %s : %ld Bytes\n", src_ip, recv_len);
show_nbns_response(res_buf);
}
该函数通过recvfrom
接收数据并存入字符数组res_buf
中,同时可以保存发送方的地址信息并通过inet_ntop
转换为点分十进制IP地址。最后通过调用函数show_nbns_response
打印解析数据。
接收过程中,要考虑到数据来源的一方不存在的情况,也就是永远收不到数据的情况,比如请求IP地址为192.168.1.10
的设备,但此刻该设备关机了。为此,我们需要设置超时时间,通过setsockopt
函数可以配置socket相关的可选项,当发生超时时,进行错误处理并退出。
打印输出
接收数据后会调用show_nbns_response
函数进行打印输出。
void show_nbns_response(char *buf)
{
struct nbns_response_header *res;
struct nbns_name *pname, *pend;
char rname[18] = {0};
char echar = '\0';
res = (struct nbns_response_header *)buf;
decode_name(res->rname, rname);
printf(
"Transaction: %d\n" \
"Flags: 0x%04x\n" \
"Questions: %d\n" \
"Answers: %d\n" \
"Authority: %d\n" \
"Additional: %d\n" \
"RR_Name: %s (%s)\n" \
"RR_Type: %s\n" \
"RR_Class: %s\n" \
"TTL: %d\n" \
"Data length: %d\n" \
"\nNumber of names: %d\n",
ntohs(res->header.tid),
ntohs(res->header.flags),
ntohs(res->header.question),
ntohs(res->header.answer),
ntohs(res->header.authority),
ntohs(res->header.additional),
res->rname + 1, rname,
ntohs(res->rtype) == NBNS_TYPE_NBSTAT ? "NBSTAT (33)" : "NB (32)",
ntohs(res->rclass) == NBNS_CLASS_IN ? "IN (1)" : "UNKNOWN",
ntohl(res->ttl),
ntohs(res->length),
res->num_of_names);
pname = (struct nbns_name *)&buf[sizeof(struct nbns_response_header)];
pend = pname + res->num_of_names;
printf("---------------------------\n");
for (; pname < pend; pname++ ) {
/* ignore the last byte of name*/
echar = pname->name[15];
pname->name[15] = '\0';
printf("Name: %s<%02x>\n", pname->name, echar);
printf("Name flags: 0x%04x\n", ntohs(pname->flags));
printf("---------------------------\n");
}
}
由于不清楚发送方究竟会发送多少组名称,所以先从缓存数组中读取头部信息,然后根据头部信息提供的num_of_names确定名称格式,再逐一打印出来。读取数据的过程就像是收件方接收到信件然后进行阅读一样。
注意,这里有用到ntohs
, ntohl
进行字节序的转换,同时有用到decode_name进行名称的解码。
异常处理
最后简单描述下异常处理函数handle_error
, 其实很简单,就是在发生错误的情况下关闭资源,打印错误信息,最后退出程序。
int nbns_sock; /* global value */
static inline void handle_error(char *msg)
{
if (nbns_sock >= 0)
close(nbns_sock);
perror(msg);
exit(EXIT_FAILURE);
}
测试
程序编写完成,使用gcc
编译即可,下面给出测试用例↓
➜ nbnstat 192.168.1.1
Send nbns query to 192.168.1.1
recv response from 192.168.1.1 : 247 Bytes
Transaction: 1
Flags: 0x8580
Questions: 0
Answers: 1
Authority: 0
Additional: 0
RR_Name: CKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA (*)
RR_Type: NBSTAT (33)
RR_Class: IN (1)
TTL: 0
Data length: 191
Number of names: 8
---------------------------
Name: SMBSHARE <00>
Name flags: 0x4400
---------------------------
Name: SMBSHARE <03>
Name flags: 0x4400
---------------------------
Name: SMBSHARE <1f>
Name flags: 0x4400
---------------------------
Name: SMBSHARE <20>
Name flags: 0x4400
---------------------------
Name: __MSBROWSE__<01>
Name flags: 0xc400
---------------------------
Name: WORKGROUP <00>
Name flags: 0xc400
---------------------------
Name: WORKGROUP <1d>
Name flags: 0x4400
---------------------------
Name: WORKGROUP <1e>
Name flags: 0xc400
---------------------------
除了flags信息使用原有的16进制表示外,其余数据均以整型或字符串形式显示。
上面是正常情况,下面给出两个异常情况的例子。
➜ nbnstat 192.168.1.10
Send nbns query to 192.168.1.10
recvfrom: Resource temporarily unavailable
➜ nbnstat 192.16
Not in presentation format
- 第1个是指定IP设备不存在,然后产生超时错误;
- 第2个是指定IP格式不正确,提示格式错误。
小结
本文介绍了一个简单NBNS客户端的具体实现,该客户端用于向指定IP设备发送NBNS NODE STATUS QUERY,并对请求结果予以分析和打印。涉及到的Socket API函数有socket, bind, sendto, recvfrom, htons, htonl, ntohs, ntohl, inet_pton, setsockopt等。
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 litreily的博客!