Import release 0.1.2
[secnet] / netlink.c
1 /* User-kernel network link */
2
3 /* Each netlink device is actually a router, with its own IP address.
4 We do things like decreasing the TTL and recalculating the header
5 checksum, generating ICMP, responding to pings, etc. */
6
7 /* This is where we have the anti-spoofing paranoia - before sending a
8 packet to the kernel we check that the tunnel it came over could
9 reasonably have produced it. */
10
11 #include "secnet.h"
12 #include "util.h"
13 #include "netlink.h"
14
15 /* Generic IP checksum routine */
16 static inline uint16_t ip_csum(uint8_t *iph,uint32_t count)
17 {
18 register uint32_t sum=0;
19
20 while (count>1) {
21 sum+=ntohs(*(uint16_t *)iph);
22 iph+=2;
23 count-=2;
24 }
25 if(count>0)
26 sum+=*(uint8_t *)iph;
27 while (sum>>16)
28 sum=(sum&0xffff)+(sum>>16);
29 return htons(~sum);
30 }
31
32 #ifdef i386
33 /*
34 * This is a version of ip_compute_csum() optimized for IP headers,
35 * which always checksum on 4 octet boundaries.
36 *
37 * By Jorge Cwik <jorge@laser.satlink.net>, adapted for linux by
38 * Arnt Gulbrandsen.
39 */
40 static inline uint16_t ip_fast_csum(uint8_t *iph, uint32_t ihl) {
41 uint32_t sum;
42
43 __asm__ __volatile__("
44 movl (%1), %0
45 subl $4, %2
46 jbe 2f
47 addl 4(%1), %0
48 adcl 8(%1), %0
49 adcl 12(%1), %0
50 1: adcl 16(%1), %0
51 lea 4(%1), %1
52 decl %2
53 jne 1b
54 adcl $0, %0
55 movl %0, %2
56 shrl $16, %0
57 addw %w2, %w0
58 adcl $0, %0
59 notl %0
60 2:
61 "
62 /* Since the input registers which are loaded with iph and ipl
63 are modified, we must also specify them as outputs, or gcc
64 will assume they contain their original values. */
65 : "=r" (sum), "=r" (iph), "=r" (ihl)
66 : "1" (iph), "2" (ihl));
67 return sum;
68 }
69 #else
70 static inline uint16_t ip_fast_csum(uint8_t *iph, uint32_t ihl)
71 {
72 return ip_csum(iph,ihl*4);
73 }
74 #endif
75
76 struct iphdr {
77 #if defined (WORDS_BIGENDIAN)
78 uint8_t version:4,
79 ihl:4;
80 #else
81 uint8_t ihl:4,
82 version:4;
83 #endif
84 uint8_t tos;
85 uint16_t tot_len;
86 uint16_t id;
87 uint16_t frag_off;
88 uint8_t ttl;
89 uint8_t protocol;
90 uint16_t check;
91 uint32_t saddr;
92 uint32_t daddr;
93 /* The options start here. */
94 };
95
96 struct icmphdr {
97 struct iphdr iph;
98 uint8_t type;
99 uint8_t code;
100 uint16_t check;
101 union {
102 uint32_t unused;
103 struct {
104 uint8_t pointer;
105 uint8_t unused1;
106 uint16_t unused2;
107 } pprob;
108 uint32_t gwaddr;
109 struct {
110 uint16_t id;
111 uint16_t seq;
112 } echo;
113 } d;
114 };
115
116 static void netlink_packet_deliver(struct netlink *st,
117 struct netlink_client *client,
118 struct buffer_if *buf);
119
120 static struct icmphdr *netlink_icmp_tmpl(struct netlink *st,
121 uint32_t dest,uint16_t len)
122 {
123 struct icmphdr *h;
124
125 BUF_ALLOC(&st->icmp,"netlink_icmp_tmpl");
126 buffer_init(&st->icmp,st->max_start_pad);
127 h=buf_append(&st->icmp,sizeof(*h));
128
129 h->iph.version=4;
130 h->iph.ihl=5;
131 h->iph.tos=0;
132 h->iph.tot_len=htons(len+(h->iph.ihl*4)+8);
133 h->iph.id=0;
134 h->iph.frag_off=0;
135 h->iph.ttl=255;
136 h->iph.protocol=1;
137 h->iph.saddr=htonl(st->secnet_address);
138 h->iph.daddr=htonl(dest);
139 h->iph.check=0;
140 h->iph.check=ip_fast_csum((uint8_t *)&h->iph,h->iph.ihl);
141 h->check=0;
142 h->d.unused=0;
143
144 return h;
145 }
146
147 /* Fill in the ICMP checksum field correctly */
148 static void netlink_icmp_csum(struct icmphdr *h)
149 {
150 uint32_t len;
151
152 len=ntohs(h->iph.tot_len)-(4*h->iph.ihl);
153 h->check=0;
154 h->check=ip_csum(&h->type,len);
155 }
156
157 /* RFC1122:
158 * An ICMP error message MUST NOT be sent as the result of
159 * receiving:
160 *
161 * * an ICMP error message, or
162 *
163 * * a datagram destined to an IP broadcast or IP multicast
164 * address, or
165 *
166 * * a datagram sent as a link-layer broadcast, or
167 *
168 * * a non-initial fragment, or
169 *
170 * * a datagram whose source address does not define a single
171 * host -- e.g., a zero address, a loopback address, a
172 * broadcast address, a multicast address, or a Class E
173 * address.
174 */
175 static bool_t netlink_icmp_may_reply(struct buffer_if *buf)
176 {
177 struct iphdr *iph;
178 uint32_t source;
179
180 iph=(struct iphdr *)buf->start;
181 if (iph->protocol==1) return False; /* Overly-broad; we may reply to
182 eg. icmp echo-request */
183 /* How do we spot broadcast destination addresses? */
184 if (ntohs(iph->frag_off)&0x1fff) return False; /* Non-initial fragment */
185 source=ntohl(iph->saddr);
186 if (source==0) return False;
187 if ((source&0xff000000)==0x7f000000) return False;
188 /* How do we spot broadcast source addresses? */
189 if ((source&0xf0000000)==0xe0000000) return False; /* Multicast */
190 if ((source&0xf0000000)==0xf0000000) return False; /* Class E */
191 return True;
192 }
193
194 /* How much of the original IP packet do we include in its ICMP
195 response? The header plus up to 64 bits. */
196 static uint16_t netlink_icmp_reply_len(struct buffer_if *buf)
197 {
198 struct iphdr *iph=(struct iphdr *)buf->start;
199 uint16_t hlen,plen;
200
201 hlen=iph->ihl*4;
202 /* We include the first 8 bytes of the packet data, provided they exist */
203 hlen+=8;
204 plen=ntohs(iph->tot_len);
205 return (hlen>plen?plen:hlen);
206 }
207
208 /* client indicates where the packet we're constructing a response to
209 comes from. NULL indicates the host. */
210 static void netlink_icmp_simple(struct netlink *st, struct buffer_if *buf,
211 struct netlink_client *client,
212 uint8_t type, uint8_t code)
213 {
214 struct iphdr *iph=(struct iphdr *)buf->start;
215 struct icmphdr *h;
216 uint16_t len;
217
218 if (netlink_icmp_may_reply(buf)) {
219 len=netlink_icmp_reply_len(buf);
220 h=netlink_icmp_tmpl(st,ntohl(iph->saddr),len);
221 h->type=type; h->code=code;
222 memcpy(buf_append(&st->icmp,len),buf->start,len);
223 netlink_icmp_csum(h);
224 netlink_packet_deliver(st,NULL,&st->icmp);
225 BUF_ASSERT_FREE(&st->icmp);
226 }
227 }
228
229 /*
230 * RFC1122: 3.1.2.2 MUST silently discard any IP frame that fails the
231 * checksum.
232 *
233 * Is the datagram acceptable?
234 *
235 * 1. Length at least the size of an ip header
236 * 2. Version of 4
237 * 3. Checksums correctly.
238 * 4. Doesn't have a bogus length
239 */
240 static bool_t netlink_check(struct netlink *st, struct buffer_if *buf)
241 {
242 struct iphdr *iph=(struct iphdr *)buf->start;
243 uint32_t len;
244
245 if (iph->ihl < 5 || iph->version != 4) return False;
246 if (buf->size < iph->ihl*4) return False;
247 if (ip_fast_csum((uint8_t *)iph, iph->ihl)!=0) return False;
248 len=ntohs(iph->tot_len);
249 /* There should be no padding */
250 if (buf->size!=len || len<(iph->ihl<<2)) return False;
251 /* XXX check that there's no source route specified */
252 return True;
253 }
254
255 /* Deliver a packet. "client" points to the _origin_ of the packet, not
256 its destination. (May be used when sending ICMP response - avoid
257 asymmetric routing.) */
258 static void netlink_packet_deliver(struct netlink *st,
259 struct netlink_client *client,
260 struct buffer_if *buf)
261 {
262 struct iphdr *iph=(struct iphdr *)buf->start;
263 uint32_t dest=ntohl(iph->daddr);
264 uint32_t source=ntohl(iph->saddr);
265 uint32_t best_quality;
266 int best_match;
267 int i;
268
269 BUF_ASSERT_USED(buf);
270
271 if (dest==st->secnet_address) {
272 Message(M_ERROR,"%s: trying to deliver a packet to myself!\n");
273 BUF_FREE(buf);
274 return;
275 }
276
277 if (!client) {
278 /* Origin of packet is host or secnet. Might be for a tunnel. */
279 best_quality=0;
280 best_match=-1;
281 for (i=0; i<st->n_routes; i++) {
282 if (st->routes[i].up && subnet_match(&st->routes[i].net,dest)) {
283 if (st->routes[i].c->link_quality>best_quality
284 || best_quality==0) {
285 best_quality=st->routes[i].c->link_quality;
286 best_match=i;
287 /* If quality isn't perfect we may wish to
288 consider kicking the tunnel with a 0-length
289 packet to prompt it to perform a key setup.
290 Then it'll eventually decide it's up or
291 down. */
292 /* If quality is perfect we don't need to search
293 any more. */
294 if (best_quality>=MAXIMUM_LINK_QUALITY) break;
295 }
296 }
297 }
298 if (best_match==-1) {
299 /* Not going down a tunnel. Might be for the host.
300 XXX think about this - only situation should be if we're
301 sending ICMP. */
302 if (source!=st->secnet_address) {
303 Message(M_ERROR,"netlink_packet_deliver: outgoing packet "
304 "from host that won't fit down any of our tunnels!\n");
305 /* XXX I think this could also occur if a soft tunnel just
306 went down, but still had packets queued in the kernel. */
307 BUF_FREE(buf);
308 } else {
309 st->deliver_to_host(st->dst,NULL,buf);
310 BUF_ASSERT_FREE(buf);
311 }
312 } else {
313 if (best_quality>0) {
314 st->routes[best_match].c->deliver(
315 st->routes[best_match].c->dst,
316 st->routes[best_match].c, buf);
317 BUF_ASSERT_FREE(buf);
318 } else {
319 /* Generate ICMP destination unreachable */
320 netlink_icmp_simple(st,buf,client,3,0); /* client==NULL */
321 BUF_FREE(buf);
322 }
323 }
324 } else { /* client is set */
325 /* We know the origin is a tunnel - packet must be for the host */
326 if (subnet_matches_list(&st->networks,dest)) {
327 st->deliver_to_host(st->dst,NULL,buf);
328 BUF_ASSERT_FREE(buf);
329 } else {
330 Message(M_ERROR,"%s: packet from tunnel %s can't be delivered "
331 "to the host\n",st->name,client->name);
332 netlink_icmp_simple(st,buf,client,3,0);
333 BUF_FREE(buf);
334 }
335 }
336 BUF_ASSERT_FREE(buf);
337 }
338
339 static void netlink_packet_forward(struct netlink *st,
340 struct netlink_client *client,
341 struct buffer_if *buf)
342 {
343 struct iphdr *iph=(struct iphdr *)buf->start;
344
345 BUF_ASSERT_USED(buf);
346
347 /* Packet has already been checked */
348 if (iph->ttl<=1) {
349 /* Generate ICMP time exceeded */
350 netlink_icmp_simple(st,buf,client,11,0);
351 BUF_FREE(buf);
352 return;
353 }
354 iph->ttl--;
355 iph->check=0;
356 iph->check=ip_fast_csum((uint8_t *)iph,iph->ihl);
357
358 netlink_packet_deliver(st,client,buf);
359 BUF_ASSERT_FREE(buf);
360 }
361
362 /* Deal with packets addressed explicitly to us */
363 static void netlink_packet_local(struct netlink *st,
364 struct netlink_client *client,
365 struct buffer_if *buf)
366 {
367 struct icmphdr *h;
368
369 h=(struct icmphdr *)buf->start;
370
371 if ((ntohs(h->iph.frag_off)&0xbfff)!=0) {
372 Message(M_WARNING,"%s: fragmented packet addressed to secnet; "
373 "ignoring it\n",st->name);
374 BUF_FREE(buf);
375 return;
376 }
377
378 if (h->iph.protocol==1) {
379 /* It's ICMP */
380 if (h->type==8 && h->code==0) {
381 /* ICMP echo-request. Special case: we re-use the buffer
382 to construct the reply. */
383 h->type=0;
384 h->iph.daddr=h->iph.saddr;
385 h->iph.saddr=htonl(st->secnet_address);
386 h->iph.ttl=255; /* Be nice and bump it up again... */
387 h->iph.check=0;
388 h->iph.check=ip_fast_csum((uint8_t *)h,h->iph.ihl);
389 netlink_icmp_csum(h);
390 netlink_packet_deliver(st,NULL,buf);
391 return;
392 }
393 Message(M_WARNING,"%s: unknown incoming ICMP\n",st->name);
394 } else {
395 /* Send ICMP protocol unreachable */
396 netlink_icmp_simple(st,buf,client,3,2);
397 BUF_FREE(buf);
398 return;
399 }
400
401 BUF_FREE(buf);
402 }
403
404 /* If cid==NULL packet is from host, otherwise cid specifies which tunnel
405 it came from. */
406 static void netlink_incoming(void *sst, void *cid, struct buffer_if *buf)
407 {
408 struct netlink *st=sst;
409 struct netlink_client *client=cid;
410 uint32_t source,dest;
411 struct iphdr *iph;
412
413 BUF_ASSERT_USED(buf);
414 if (!netlink_check(st,buf)) {
415 Message(M_WARNING,"%s: bad IP packet from %s\n",
416 st->name,client?client->name:"host");
417 BUF_FREE(buf);
418 return;
419 }
420 iph=(struct iphdr *)buf->start;
421
422 source=ntohl(iph->saddr);
423 dest=ntohl(iph->daddr);
424
425 /* Check source */
426 if (client) {
427 /* Check that the packet source is in 'nets' and its destination is
428 in st->networks */
429 if (!subnet_matches_list(client->networks,source)) {
430 string_t s,d;
431 s=ipaddr_to_string(source);
432 d=ipaddr_to_string(dest);
433 Message(M_WARNING,"%s: packet from tunnel %s with bad "
434 "source address (s=%s,d=%s)\n",st->name,client->name,s,d);
435 free(s); free(d);
436 BUF_FREE(buf);
437 return;
438 }
439 } else {
440 if (!subnet_matches_list(&st->networks,source)) {
441 string_t s,d;
442 s=ipaddr_to_string(source);
443 d=ipaddr_to_string(dest);
444 Message(M_WARNING,"%s: outgoing packet with bad source address "
445 "(s=%s,d=%s)\n",st->name,s,d);
446 free(s); free(d);
447 BUF_FREE(buf);
448 return;
449 }
450 }
451 /* (st->secnet_address needs checking before matching destination
452 addresses) */
453 if (dest==st->secnet_address) {
454 netlink_packet_local(st,client,buf);
455 BUF_ASSERT_FREE(buf);
456 return;
457 }
458 if (client) {
459 if (!subnet_matches_list(&st->networks,dest)) {
460 string_t s,d;
461 s=ipaddr_to_string(source);
462 d=ipaddr_to_string(dest);
463 Message(M_WARNING,"%s: incoming packet from tunnel %s "
464 "with bad destination address "
465 "(s=%s,d=%s)\n",st->name,client->name,s,d);
466 free(s); free(d);
467 BUF_FREE(buf);
468 return;
469 }
470 }
471 netlink_packet_forward(st,client,buf);
472 BUF_ASSERT_FREE(buf);
473 }
474
475 static void netlink_set_softlinks(struct netlink *st, struct netlink_client *c,
476 bool_t up)
477 {
478 uint32_t i;
479
480 if (!st->routes) return; /* Table has not yet been created */
481 for (i=0; i<st->n_routes; i++) {
482 if (!st->routes[i].hard && st->routes[i].c==c) {
483 st->routes[i].up=up;
484 st->set_route(st->dst,&st->routes[i]);
485 }
486 }
487 }
488
489 static void netlink_set_quality(void *sst, void *cid, uint32_t quality)
490 {
491 struct netlink *st=sst;
492 struct netlink_client *c=cid;
493
494 c->link_quality=quality;
495 if (c->link_quality==LINK_QUALITY_DOWN) {
496 netlink_set_softlinks(st,c,False);
497 } else {
498 netlink_set_softlinks(st,c,True);
499 }
500 }
501
502 static void *netlink_regnets(void *sst, struct subnet_list *nets,
503 netlink_deliver_fn *deliver, void *dst,
504 uint32_t max_start_pad, uint32_t max_end_pad,
505 bool_t hard_routes, string_t client_name)
506 {
507 struct netlink *st=sst;
508 struct netlink_client *c;
509
510 Message(M_DEBUG_CONFIG,"netlink_regnets: request for %d networks, "
511 "max_start_pad=%d, max_end_pad=%d\n",
512 nets->entries,max_start_pad,max_end_pad);
513
514 if (!hard_routes && !st->set_route) {
515 Message(M_ERROR,"%s: this netlink device does not support "
516 "soft routes.\n");
517 return NULL;
518 }
519
520 if (!hard_routes) {
521 /* XXX for now we assume that soft routes require root privilege;
522 this may not always be true. */
523 require_root_privileges=True;
524 require_root_privileges_explanation="netlink: soft routes";
525 }
526
527 #if 0
528 /* XXX POLICY: do we check nets against local networks? If we do,
529 that prevents things like laptop tunnels working. Perhaps we
530 can have a configuration option for this. Or, if the admin
531 really doesn't want remote sites to be able to claim local
532 addresses, he can list them in exclude-remote-networks. */
533 if (subnet_lists_intersect(&st->networks,nets)) {
534 Message(M_ERROR,"%s: site %s specifies networks that "
535 "intersect with our local networks\n",st->name,client_name);
536 return False;
537 }
538 #endif /* 0 */
539 /* Check that nets do not intersect st->exclude_remote_networks;
540 refuse to register if they do. */
541 if (subnet_lists_intersect(&st->exclude_remote_networks,nets)) {
542 Message(M_ERROR,"%s: site %s specifies networks that "
543 "intersect with the explicitly excluded remote networks\n",
544 st->name,client_name);
545 return False;
546 }
547
548 c=safe_malloc(sizeof(*c),"netlink_regnets");
549 c->networks=nets;
550 c->deliver=deliver;
551 c->dst=dst;
552 c->name=client_name; /* XXX copy it? */
553 c->hard_routes=hard_routes;
554 c->link_quality=LINK_QUALITY_DOWN;
555 c->next=st->clients;
556 st->clients=c;
557 if (max_start_pad > st->max_start_pad) st->max_start_pad=max_start_pad;
558 if (max_end_pad > st->max_end_pad) st->max_end_pad=max_end_pad;
559 st->n_routes+=nets->entries;
560
561 return c;
562 }
563
564 static void netlink_dump_routes(struct netlink *st)
565 {
566 int i;
567 string_t net;
568
569 Message(M_INFO,"%s: routing table:\n",st->name);
570 for (i=0; i<st->n_routes; i++) {
571 net=subnet_to_string(&st->routes[i].net);
572 Message(M_INFO,"%s -> tunnel %s (%s,%s)\n",net,st->routes[i].c->name,
573 st->routes[i].hard?"hard":"soft",
574 st->routes[i].up?"up":"down");
575 free(net);
576 }
577 Message(M_INFO,"%s/32 -> netlink \"%s\"\n",
578 ipaddr_to_string(st->secnet_address),st->name);
579 for (i=0; i<st->networks.entries; i++) {
580 net=subnet_to_string(&st->networks.list[i]);
581 Message(M_INFO,"%s -> host\n",net);
582 free(net);
583 }
584 }
585
586 static int netlink_compare_route_specificity(const void *ap, const void *bp)
587 {
588 const struct netlink_route *a=ap;
589 const struct netlink_route *b=bp;
590
591 if (a->net.len==b->net.len) return 0;
592 if (a->net.len<b->net.len) return 1;
593 return -1;
594 }
595
596 static void netlink_phase_hook(void *sst, uint32_t new_phase)
597 {
598 struct netlink *st=sst;
599 struct netlink_client *c;
600 uint32_t i,j;
601
602 /* All the networks serviced by the various tunnels should now
603 * have been registered. We build a routing table by sorting the
604 * routes into most-specific-first order. */
605 st->routes=safe_malloc(st->n_routes*sizeof(*st->routes),
606 "netlink_phase_hook");
607 /* Fill the table */
608 i=0;
609 for (c=st->clients; c; c=c->next) {
610 for (j=0; j<c->networks->entries; j++) {
611 st->routes[i].net=c->networks->list[j];
612 st->routes[i].c=c;
613 st->routes[i].up=c->hard_routes; /* Hard routes are always up;
614 soft routes default to down */
615 st->routes[i].kup=False;
616 st->routes[i].hard=c->hard_routes;
617 i++;
618 }
619 }
620 /* ASSERT i==st->n_routes */
621 if (i!=st->n_routes) {
622 fatal("netlink: route count error: expected %d got %d\n",
623 st->n_routes,i);
624 }
625 /* Sort the table in descending order of specificity */
626 qsort(st->routes,st->n_routes,sizeof(*st->routes),
627 netlink_compare_route_specificity);
628
629 netlink_dump_routes(st);
630 }
631
632 netlink_deliver_fn *netlink_init(struct netlink *st,
633 void *dst, struct cloc loc,
634 dict_t *dict, string_t description,
635 netlink_route_fn *set_route,
636 netlink_deliver_fn *to_host)
637 {
638 st->dst=dst;
639 st->cl.description=description;
640 st->cl.type=CL_NETLINK;
641 st->cl.apply=NULL;
642 st->cl.interface=&st->ops;
643 st->ops.st=st;
644 st->ops.regnets=netlink_regnets;
645 st->ops.deliver=netlink_incoming;
646 st->ops.set_quality=netlink_set_quality;
647 st->max_start_pad=0;
648 st->max_end_pad=0;
649 st->clients=NULL;
650 st->set_route=set_route;
651 st->deliver_to_host=to_host;
652
653 st->name=dict_read_string(dict,"name",False,"netlink",loc);
654 if (!st->name) st->name=description;
655 dict_read_subnet_list(dict, "networks", True, "netlink", loc,
656 &st->networks);
657 dict_read_subnet_list(dict, "exclude-remote-networks", False, "netlink",
658 loc, &st->exclude_remote_networks);
659 /* local-address and secnet-address do not have to be in local-networks;
660 however, they should be advertised in the 'sites' file for the
661 local site. */
662 st->local_address=string_to_ipaddr(
663 dict_find_item(dict,"local-address", True, "netlink", loc),"netlink");
664 st->secnet_address=string_to_ipaddr(
665 dict_find_item(dict,"secnet-address", True, "netlink", loc),"netlink");
666 st->mtu=dict_read_number(dict, "mtu", False, "netlink", loc, DEFAULT_MTU);
667 buffer_new(&st->icmp,ICMP_BUFSIZE);
668 st->n_routes=0;
669 st->routes=NULL;
670
671 add_hook(PHASE_SETUP,netlink_phase_hook,st);
672
673 return netlink_incoming;
674 }
675
676 /* No connection to the kernel at all... */
677
678 struct null {
679 struct netlink nl;
680 };
681
682 static bool_t null_set_route(void *sst, struct netlink_route *route)
683 {
684 struct null *st=sst;
685 string_t t;
686
687 if (route->up!=route->kup) {
688 t=subnet_to_string(&route->net);
689 Message(M_INFO,"%s: setting route %s to state %s\n",st->nl.name,
690 t, route->up?"up":"down");
691 free(t);
692 route->kup=route->up;
693 return True;
694 }
695 return False;
696 }
697
698 static void null_deliver(void *sst, void *cid, struct buffer_if *buf)
699 {
700 return;
701 }
702
703 static list_t *null_apply(closure_t *self, struct cloc loc, dict_t *context,
704 list_t *args)
705 {
706 struct null *st;
707 item_t *item;
708 dict_t *dict;
709
710 st=safe_malloc(sizeof(*st),"null_apply");
711
712 item=list_elem(args,0);
713 if (!item || item->type!=t_dict)
714 cfgfatal(loc,"null-netlink","parameter must be a dictionary\n");
715
716 dict=item->data.dict;
717
718 netlink_init(&st->nl,st,loc,dict,"null-netlink",null_set_route,
719 null_deliver);
720
721 return new_closure(&st->nl.cl);
722 }
723
724 init_module netlink_module;
725 void netlink_module(dict_t *dict)
726 {
727 add_closure(dict,"null-netlink",null_apply);
728 }