Libevent 提供了一些用于解析DNS域名的API, 以及一些用于实现DNS Server的接口。
Libevent 提供了标准库函数getaddrinfo
的可移植版本,用于让阻塞性域名解析应用于可移植程序。(毕竟有些平台并不支持getaddrinfo
函数,或者该函数相对于libevent的替代接口表现更差)。
struct evutil_addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct evutil_addrinfo *ai_next;
};
#define EVUTIL_AI_PASSIVE /* ... */
#define EVUTIL_AI_CANONNAME /* ... */
#define EVUTIL_AI_NUMERICHOST /* ... */
#define EVUTIL_AI_NUMERICSERV /* ... */
#define EVUTIL_AI_V4MAPPED /* ... */
#define EVUTIL_AI_ALL /* ... */
#define EVUTIL_AI_ADDRCONFIG /* ... */
int evutil_getaddrinfo(const char *nodename, const char *servname,
const struct evutil_addrinfo *hints, struct evutil_addrinfo **res);
void evutil_freeaddrinfo(struct evutil_addrinfo *ai);
const char *evutil_gai_strerror(int err);
www.example.com
等,如果值为控偶,则表示localhost
(默认情况下),或者·any·(EVUTIL_AI_PASSIVE
)被设置的情况下https
或者端口号。如果没有指定servname,则*res中的port会被设置为0.在解析规则hints
中,包含了成员ai_flag
,它指定了如何进行lookup(查询)。下面是一些常用的值:
EVUTIL_EAI_NONAME
错误。ai_family
字段被指定为AF_INET6, 而又解析不到IPv6的地址,那么这个解析到的IPv4的地址将作为IPv6地址的映射返回。(取决于系统是否支持。)EVUTIL_AI_V4MAPPED
都被设置了,那么不管有没有解析到IPv6的地址, 在解析结果中的IPv4地址都会作为4-mapped IPv6地址形式被保存。如果evutil_getaddrinfo
解析成功了,则它会分配一个节点为evutil_addrinfo structures
类型的链表res,用于保存每一个解析到的地址。这些内存是从堆中分配的,可以通过evutil_freeaddrinfo
释放内存。
如果evutil_getaddrinfo
解析失败了,则它会返回一个错误码(如EVUTIL_EAI_ADDRFAMILY
)可以通过evutil_gai_strerror
解析。
// Example
#include
#include
#include
#include
#include
#include
#include
evutil_socket_t
get_tcp_socket_for_host(const char *hostname, ev_uint16_t port)
{
char port_buf[6];
struct evutil_addrinfo hints;
struct evutil_addrinfo *answer = NULL;
int err;
evutil_socket_t sock;
/* Convert the port to decimal. */
evutil_snprintf(port_buf, sizeof(port_buf), "%d", (int)port);
/* Build the hints to tell getaddrinfo how to act. */
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* v4 or v6 is fine. */
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP; /* We want a TCP socket */
/* Only return addresses we can use. */
hints.ai_flags = EVUTIL_AI_ADDRCONFIG;
/* Look up the hostname. */
err = evutil_getaddrinfo(hostname, port_buf, &hints, &answer);
if (err != 0) {
fprintf(stderr, "Error while resolving '%s': %s",
hostname, evutil_gai_strerror(err));
return -1;
}
/* If there was no error, we should have at least one answer. */
assert(answer);
/* Just use the first answer. */
sock = socket(answer->ai_family,
answer->ai_socktype,
answer->ai_protocol);
if (sock < 0)
return -1;
if (connect(sock, answer->ai_addr, answer->ai_addrlen)) {
/* Note that we're doing a blocking connect in this function.
* If this were nonblocking, we'd need to treat some errors
* (like EINTR and EAGAIN) specially. */
EVUTIL_CLOSESOCKET(sock);
return -1;
}
return sock;
}
不管是标准的getaddrinfo
还是我们前面介绍的evutil_getaddrinfo
都存在一个问题:它们都是阻塞的。
然而,我们一般讲Libevent应用于非阻塞环境中,因而最好有一个非阻塞的域名解析接口。
庆幸的是,Libevent确实提供了非阻塞的域名解析接口。
typedef void (*evdns_getaddrinfo_cb)(
int result, struct evutil_addrinfo *res, void *arg);
struct evdns_getaddrinfo_request;
struct evdns_getaddrinfo_request *evdns_getaddrinfo(
struct evdns_base *dns_base,
const char *nodename, const char *servname,
const struct evutil_addrinfo *hints_in,
evdns_getaddrinfo_cb cb, void *arg);
void evdns_getaddrinfo_cancel(struct evdns_getaddrinfo_request *req);
从函数原型中, 不难看出,evdns_getaddrinfo
的使用方法与其他使用回调函数实现非阻塞的函数类似的。其参数的作用可以参考阻塞型的evutil_getaddrinfo
。
evdns_getaddrinfo有三种返回值:
//Example:
#include
#include
#include
#include
#include
#include
#include
#include
int n_pending_requests = 0;
struct event_base *base = NULL;
struct user_data {
char *name; /* the name we're resolving */
int idx; /* its position on the command line */
};
void callback(int errcode, struct evutil_addrinfo *addr, void *ptr)
{
struct user_data *data = ptr;
const char *name = data->name;
if (errcode) {
printf("%d. %s -> %s\n", data->idx, name, evutil_gai_strerror(errcode));
} else {
struct evutil_addrinfo *ai;
printf("%d. %s", data->idx, name);
if (addr->ai_canonname)
printf(" [%s]", addr->ai_canonname);
puts("");
for (ai = addr; ai; ai = ai->ai_next) {
char buf[128];
const char *s = NULL;
if (ai->ai_family == AF_INET) {
struct sockaddr_in *sin = (struct sockaddr_in *)ai->ai_addr;
s = evutil_inet_ntop(AF_INET, &sin->sin_addr, buf, 128);
} else if (ai->ai_family == AF_INET6) {
struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ai->ai_addr;
s = evutil_inet_ntop(AF_INET6, &sin6->sin6_addr, buf, 128);
}
if (s)
printf(" -> %s\n", s);
}
evutil_freeaddrinfo(addr);
}
free(data->name);
free(data);
if (--n_pending_requests == 0)
event_base_loopexit(base, NULL);
}
/* Take a list of domain names from the command line and resolve them in
* parallel. */
int main(int argc, char **argv)
{
int i;
struct evdns_base *dnsbase;
if (argc == 1) {
puts("No addresses given.");
return 0;
}
base = event_base_new();
if (!base)
return 1;
dnsbase = evdns_base_new(base, 1);
if (!dnsbase)
return 2;
for (i = 1; i < argc; ++i) {
struct evutil_addrinfo hints;
struct evdns_getaddrinfo_request *req;
struct user_data *user_data;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_flags = EVUTIL_AI_CANONNAME;
/* Unless we specify a socktype, we'll get at least two entries for
* each address: one for TCP and one for UDP. That's not what we
* want. */
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
if (!(user_data = malloc(sizeof(struct user_data)))) {
perror("malloc");
exit(1);
}
if (!(user_data->name = strdup(argv[i]))) {
perror("strdup");
exit(1);
}
user_data->idx = i;
++n_pending_requests;
req = evdns_getaddrinfo(
dnsbase, argv[i], NULL /* no service name given */,
&hints, callback, user_data);
if (req == NULL) {
printf(" [request for %s returned immediately]\n", argv[i]);
/* No need to free user_data or decrement n_pending_requests; that
* happened in the callback. */
}
}
if (n_pending_requests)
event_base_dispatch(base);
evdns_base_free(dnsbase, 0);
event_base_free(base);
return 0;
}
在使用evdns_getaddrinfo
之前,我们需要先配置一个evdns_base
用于保存nameservers
列表、DNS 配置选项、追踪活跃的和准备发射的DNS请求。
struct evdns_base *evdns_base_new(struct event_base *event_base,
int initialize);
void evdns_base_free(struct evdns_base *base, int fail_requests);
Libevent提供了根据配置文件来初始化evdns的方法:
#define DNS_OPTION_SEARCH 1
#define DNS_OPTION_NAMESERVERS 2
#define DNS_OPTION_MISC 4
#define DNS_OPTION_HOSTSFILE 8
#define DNS_OPTIONS_ALL 15
int evdns_base_resolv_conf_parse(struct evdns_base *base, int flags,
const char *filename);
#ifdef WIN32
int evdns_base_config_windows_nameservers(struct evdns_base *);
#define EVDNS_BASE_CONFIG_WINDOWS_NAMESERVERS_IMPLEMENTED
#endif
evdns_base_resolv_conf_parse()函数扫描resolv.conf格式的文件filename,从中读取flags指示的选项(关于resolv.conf文件的更多信息,请看Unix手册)。
Libevent 同样也提供了手动配置evdns的方法,以便更加灵活地运用dvdns
int evdns_base_nameserver_sockaddr_add(struct evdns_base *base,
const struct sockaddr *sa, ev_socklen_t len,
unsigned flags);
int evdns_base_nameserver_ip_add(struct evdns_base *base,
const char *ip_as_string);
int evdns_base_load_hosts(struct evdns_base *base, const char *hosts_fname);
void evdns_base_search_clear(struct evdns_base *base);
void evdns_base_search_add(struct evdns_base *base, const char *domain);
void evdns_base_search_ndots_set(struct evdns_base *base, int ndots);
int evdns_base_set_option(struct evdns_base *base, const char *option,
const char *val);
int evdns_base_count_nameservers(struct evdns_base *base);
evdns_base_nameserver_sockaddr_add
函数:通过socket
地址的形式,向evdns_base
添加一个域名服务器。flags
参数目前是被忽略的,而且为了向前兼容性应该置为0。该函数成功是返回0,失败时返回负数。evdns_base_nameserver_ip_add
函数:也是向evdns_base添加域名服务器。不过它是以字符串的形式添加,该字符串可以是一个IPv4地址,一个IPv6地址,一个有端口号的IPv4地址(IPv4:Port),或者一个有端口号的IPv6地址([IPv6]:port)。该函数成功时返回0,失败时返回负数。evdns_base_load_hosts
函数:从hosts_name中加载一个主机文件(类似于/etc/hosts的格式)。该函数成功时返回0,失败时返回负数。evdns_base_search_clear
函数:从evdns_base中清除所有当前的search后缀(就像search选项中配置的那样);evdns_base_search_add
函数:则增加一个后缀。evdns_base_set_option
函数:向evdns_base添加一个选项key和该选项的值value,key和value都是字符串的形式。(2.0.3版本之前,选项名后面必须有一个冒号)evdns_base_count_nameservers
函数:解析一系列的配置文件之后,如果希望看到是否已经添加了域名服务器,可以使用这个函数来查看有多少个域名服务器。Libevent提供了一对函数可以对evdns模块进行库级别的设置:
typedef void (*evdns_debug_log_fn_type)(int is_warning, const char *msg);
void evdns_set_log_fn(evdns_debug_log_fn_type fn);
void evdns_set_transaction_id_fn(ev_uint16_t (*fn)(void));
由于历史的原因,evdns
子系统具有其自己独立的日志功能;可以使用evdns_set_log_fn
函数添加回调函数,对消息进行处理。
出于安全性的考虑,evdns
需要一个好的随机数源:在使用0x20 hack
时,它被用来获取难以被猜中的事务ID,从而用来随机化查询(参考randomize-case
选项)。然而老版本的Libevent,并没有提供一个安全的RNG。可以通过调用evdns_set_transaction_id_fn
,并向其提供一个能够返回难以预测的2字节无符号整数的函数,来为evdns设置一个更好的随机数产生器
在Libevent2.0.4-alpha及其之后的版本,Libevent使用自己的内部的安全RNG;所以evdns_set_transaction_id_fn
函数不在起作用。
Libevent提供了更加底层的接口,以便用来控制进行特别的DNS请求。
#define DNS_QUERY_NO_SEARCH /* ... */
#define DNS_IPv4_A /* ... */
#define DNS_PTR /* ... */
#define DNS_IPv6_AAAA /* ... */
typedef void (*evdns_callback_type)(int result, char type, int count,
int ttl, void *addresses, void *arg);
struct evdns_request *evdns_base_resolve_ipv4(struct evdns_base *base,
const char *name, int flags, evdns_callback_type callback, void *ptr);
struct evdns_request *evdns_base_resolve_ipv6(struct evdns_base *base,
const char *name, int flags, evdns_callback_type callback, void *ptr);
struct evdns_request *evdns_base_resolve_reverse(struct evdns_base *base,
const struct in_addr *in, int flags, evdns_callback_type callback,
void *ptr);
struct evdns_request *evdns_base_resolve_reverse_ipv6(
struct evdns_base *base, const struct in6_addr *in, int flags,
evdns_callback_type callback, void *ptr);
这些解析函数为一个特殊的记录发起DNS请求。每一个函数都使用·evdns_base·来进行请求,函数参数还包括需要查询的源(既可以是正向查找时的主机名,也可以是反向查找时的一个地址),一系列决定如何进行查找的标志集合,当查找结束时调用的回调函数,以及一个用户提供的传递给回调函数的参数的指针。
flags参数可以是0,或者是DNS_QUERY_NO_SEARCH
,DNS_QUERY_NO_SEARCH
表示在原始搜索失败的时候,明确禁止在search列表中进行搜索。DNS_QUERY_NO_SEARCH
标志对于反向查询没有意义,因为反向查询不会进行搜索。
当请求完成时(成功或失败),就会调用回调函数。回调函数的参数有:result表明成功或者是一个错误码(参看下面的DNS错误码),一个记录类型(DNS_IPv4_A,DNS_IPv6_AAAA或 DNS_PTR之一),addresses是记录的个数,ttl秒数,地址本身以及用户提供的参数指针。
如果发生了错误,则回调函数的addresses参数为NULL。如果没有发生错误,对于PTR记录来说,它是一个NULL结尾的字符串。对于IPv4记录来说,它是一个网络字节序的4字节的数组。对于IPv6来说,则是一个网络字节序的16字节数组。(注意,即使没有错误,addresses的数量也可以是0。比如名字存在,但是却没有请求类型的记录)
可以传递的回调函数的错误码如下:
可以使用下面的接口将错误码转换为可读的字符串:
const char *evdns_err_to_string(int err);
每一个解析函数都返回一个不透明的evdns_request结构,可以使用该结构,在回调函数调用之前的任一时刻取消请求:
void evdns_cancel_request(struct evdns_base *base, struct evdns_request *req);
使用该函数取消一个请求,会使得回调函数以DNS_ERR_CANCEL为结果码被调用。
有时希望能够在不太影响正在进行的DNS请求的情况下,对DNS子系统重新配置或者关闭。
int evdns_base_clear_nameservers_and_suspend(struct evdns_base *base);
int evdns_base_resume(struct evdns_base *base);
如果在evdns_base上调用函数evdns_base_clear_nameservers_and_suspend,则所有的域名服务器都会被删除,并且未决的请求会被保留,直到重新添加域名服务器并且调用evdns_name_resume为止。
这些函数成功时返回0,失败时返回-1。
Libevent提供了简单的功能实现一个普通的DNS服务器,并且对UDP的DNS请求进行应答。本节的内容需要你熟悉一些DNS协议。
struct evdns_server_port *evdns_add_server_port_with_base(
struct event_base *base,
evutil_socket_t socket,
int flags,
evdns_request_callback_fn_type callback, void *user_data);
typedef void (*evdns_request_callback_fn_type)(
struct evdns_server_request *request,
void *user_data);
void evdns_close_server_port(struct evdns_server_port *port);
调用evdns_add_server_port_with_base
开始监听DNS请求。该函数的参数有:一个处理事件的event_base
,一个用来监听的UDP socket,flags变量(目前总是为0);当收到新的DNS请求时调用的回调函数;一个用户提供的回调函数的参数指针。该函数返回一个新的evdns_server_port
对象。
当DNS 服务器的工作完成时,可以将该对象传递给evdns_close_server_port
函数进行关闭。
不幸的是,Libevent目前没有为通过可编程的接口来获取DNS请求提供一个很好的方式。相反的,需要包含event2/dns_struct.h,并且手动检测evdns_server_quest
结构。
将来版本的Libevent如果能提供一个更好的操作方式的话就好了。
struct evdns_server_request {
int flags;
int nquestions;
struct evdns_server_question **questions;
};
#define EVDNS_QTYPE_AXFR 252
#define EVDNS_QTYPE_ALL 255
struct evdns_server_question {
int type;
int dns_question_class;
char name[1];
};
请求的flags字段包含了请求中设置的DNS标志;nquestions是请求中包含的问题数;questions是指向结构体evdns_server_question的指针数组。每一个evdns_server_question都包含了请求的资源类型(参考下面的EVDNS_*_TYPE宏),请求的类型(典型的是EVDNS_CLASS_INET)以及请求主机名的名称。
int evdns_server_request_get_requesting_addr(struct evdns_server_request *req,
struct sockaddr *sa, int addr_len);
有时需要知道某特定的DNS请求来自何方。可以通过调用函数evdns_server_request_get_requestion_addr
函数来获得。需要传递一个足够大小的sockaddr来保存地址:建议使用sockaddr_storage
结构。
DNS服务器每收到一个请求,都会将该请求,连同用户提供的指针,一起传递到回调函数中。该回调函数必须要么能响应请求或者忽略请求,要么保证该请求最终会被应答或者忽略。
在应答请求之前,可以向应答中添加一个或多个答案:
int evdns_server_request_add_a_reply(struct evdns_server_request *req,
const char *name, int n, const void *addrs, int ttl);
int evdns_server_request_add_aaaa_reply(struct evdns_server_request *req,
const char *name, int n, const void *addrs, int ttl);
int evdns_server_request_add_cname_reply(struct evdns_server_request *req,
const char *name, const char *cname, int ttl);
上述函数会添加一个单独的RR(A,AAAA类型或者CNAME)到req请求的应答中。每个函数中,参数name就是要添加到答案中的主机名,ttl就是答案中的生存时间秒数。对于A以及AAAA记录来说,n就是需要添加的地址个数,addrs是指向原始地址的指针,它要么是在A记录中的4字节的IPv4地址,要么是AAAA记录中的16字节的IPv6地址。
这些函数成功时返回0,失败时返回-1。
int evdns_server_request_add_ptr_reply(struct evdns_server_request *req,
struct in_addr *in, const char *inaddr_name, const char *hostname,
int ttl);
该函数向请求的应答中添加一个PTR记录。req和ttl参数类似于上面的参数。必须以一个in(IPv4地址)或者inaddr_name(在.arpa域中的地址)表明要应答的那个地址。hostname参数就是PTR查询的答案。
#define EVDNS_ANSWER_SECTION 0
#define EVDNS_AUTHORITY_SECTION 1
#define EVDNS_ADDITIONAL_SECTION 2
#define EVDNS_TYPE_A 1
#define EVDNS_TYPE_NS 2
#define EVDNS_TYPE_CNAME 5
#define EVDNS_TYPE_SOA 6
#define EVDNS_TYPE_PTR 12
#define EVDNS_TYPE_MX 15
#define EVDNS_TYPE_TXT 16
#define EVDNS_TYPE_AAAA 28
#define EVDNS_CLASS_INET 1
int evdns_server_request_add_reply(struct evdns_server_request *req,
int section, const char *name, int type, int dns_class, int ttl,
int datalen, int is_name, const char *data);
该函数向req请求的DNS应答添加任意的RR。section参数表明需要添加哪一部分,而且该值必须是EVDNS_*_SECTION的其中之一。name参数是RR中的name字段。type参数是RR中的type字段,而且可能的话应该是EVDNS_TYPE_*的其中之一。dns_class参数是RR中的class字段,而且一般应该是EVDNS_CLASS_INET。ttl参数是RR中的存活时间字段。RR中的rdata和rdlength字段将会由data中的datalen个字节产生。如果is_name为true,则data将会编码为一个DNS名,否则,它将按照字面意思包含到RR中。
int evdns_server_request_respond(struct evdns_server_request *req, int err);
int evdns_server_request_drop(struct evdns_server_request *req);
evdns_server_request_respond函数发送一个DNS回应,包含所有RRs,以及错误码err。如果收到一个不想应答的请求,可以通过调用evdns_server_request_drop函数忽略它,从而可以释放所有相关内存以及记录结构。
#define EVDNS_FLAGS_AA 0x400
#define EVDNS_FLAGS_RD 0x080
void evdns_server_request_set_flags(struct evdns_server_request *req, int flags);
如果需要在应答消息中设置任何标志,可以在发送应答之前的任意时间调用该函数。
#include
#include
#include
#include
#include
#include
#include
#include
/* Let's try binding to 5353. Port 53 is more traditional, but on most
operating systems it requires root privileges. */
#define LISTEN_PORT 5353
#define LOCALHOST_IPV4_ARPA "1.0.0.127.in-addr.arpa"
#define LOCALHOST_IPV6_ARPA ("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0." \
"0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa")
const ev_uint8_t LOCALHOST_IPV4[] = { 127, 0, 0, 1 };
const ev_uint8_t LOCALHOST_IPV6[] = { 0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,1 };
#define TTL 4242
/* This toy DNS server callback answers requests for localhost (mapping it to
127.0.0.1 or ::1) and for 127.0.0.1 or ::1 (mapping them to localhost).
*/
void server_callback(struct evdns_server_request *request, void *data)
{
int i;
int error=DNS_ERR_NONE;
/* We should try to answer all the questions. Some DNS servers don't do
this reliably, though, so you should think hard before putting two
questions in one request yourself. */
for (i=0; i < request->nquestions; ++i) {
const struct evdns_server_question *q = request->questions[i];
int ok=-1;
/* We don't use regular strcasecmp here, since we want a locale-
independent comparison. */
if (0 == evutil_ascii_strcasecmp(q->name, "localhost")) {
if (q->type == EVDNS_TYPE_A)
ok = evdns_server_request_add_a_reply(
request, q->name, 1, LOCALHOST_IPV4, TTL);
else if (q->type == EVDNS_TYPE_AAAA)
ok = evdns_server_request_add_aaaa_reply(
request, q->name, 1, LOCALHOST_IPV6, TTL);
} else if (0 == evutil_ascii_strcasecmp(q->name, LOCALHOST_IPV4_ARPA)) {
if (q->type == EVDNS_TYPE_PTR)
ok = evdns_server_request_add_ptr_reply(
request, NULL, q->name, "LOCALHOST", TTL);
} else if (0 == evutil_ascii_strcasecmp(q->name, LOCALHOST_IPV6_ARPA)) {
if (q->type == EVDNS_TYPE_PTR)
ok = evdns_server_request_add_ptr_reply(
request, NULL, q->name, "LOCALHOST", TTL);
} else {
error = DNS_ERR_NOTEXIST;
}
if (ok<0 && error==DNS_ERR_NONE)
error = DNS_ERR_SERVERFAILED;
}
/* Now send the reply. */
evdns_server_request_respond(request, error);
int main(int argc, char **argv)
{
struct event_base *base;
struct evdns_server_port *server;
evutil_socket_t server_fd;
struct sockaddr_in listenaddr;
base = event_base_new();
if (!base)
return 1;
server_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (server_fd < 0)
return 2;
memset(&listenaddr, 0, sizeof(listenaddr));
listenaddr.sin_family = AF_INET;
listenaddr.sin_port = htons(LISTEN_PORT);
listenaddr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_fd, (struct sockaddr*)&listenaddr, sizeof(listenaddr))<0)
return 3;
/*The server will hijack the event loop after receiving the first request if the socket is blocking*/
if(evutil_make_socket_nonblocking(server_fd)<0)
return 4;
server = evdns_add_server_port_with_base(base, server_fd, 0,
server_callback, NULL);
event_base_dispatch(base);
evdns_close_server_port(server);
event_base_free(base);
return 0;
}