Browse Source

Initial commit

Steven Barth 10 years ago
commit
8b19de666e
19 changed files with 5445 additions and 0 deletions
  1. 14 0
      .gitignore
  2. 31 0
      CMakeLists.txt
  3. 340 0
      COPYING
  4. 85 0
      README
  5. 561 0
      src/config.c
  6. 508 0
      src/dhcpv4.c
  7. 93 0
      src/dhcpv4.h
  8. 893 0
      src/dhcpv6-ia.c
  9. 452 0
      src/dhcpv6.c
  10. 162 0
      src/dhcpv6.h
  11. 242 0
      src/md5.c
  12. 17 0
      src/md5.h
  13. 532 0
      src/ndp.c
  14. 31 0
      src/ndp.h
  15. 423 0
      src/odhcpd.c
  16. 205 0
      src/odhcpd.h
  17. 502 0
      src/router.c
  18. 40 0
      src/router.h
  19. 314 0
      src/ubus.c

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+.project
+.cproject
+odhcpd
+config.log
+CMakeCache.txt
+CMakeFiles
+CPackConfig.cmake
+CPackSourceConfig.cmake
+_CPack_Packages
+Makefile
+cmake_install.cmake
+install_manifest.txt
+*.deb
+

+ 31 - 0
CMakeLists.txt

@@ -0,0 +1,31 @@
+cmake_minimum_required(VERSION 2.8)
+cmake_policy(SET CMP0015 NEW)
+
+# Project Definition
+project(odhcpd C)
+
+set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -std=c99")
+
+
+add_definitions(-D_GNU_SOURCE -Wall -Werror -Wextra -DWITH_UBUS)
+
+add_executable(odhcpd src/odhcpd.c src/config.c src/router.c src/dhcpv6.c src/ndp.c src/md5.c src/dhcpv6-ia.c src/dhcpv4.c src/ubus.c)
+target_link_libraries(odhcpd resolv ubus ubox uci)
+
+# Installation
+install(TARGETS odhcpd DESTINATION sbin/)
+
+
+# Packaging information
+set(CPACK_PACKAGE_VERSION "1")
+set(CPACK_PACKAGE_CONTACT "Steven Barth <steven@midlink.org>")
+set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "odhcpd")
+set(CPACK_GENERATOR "DEB;RPM;STGZ")
+set(CPACK_STRIP_FILES true)
+
+SET(CPACK_DEBIAN_PACKAGE_VERSION ${CPACK_PACKAGE_VERSION})
+set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}_${CPACK_DEBIAN_PACKAGE_VERSION}")
+
+include(CPack)
+

+ 340 - 0
COPYING

@@ -0,0 +1,340 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+		     END OF TERMS AND CONDITIONS
+
+	    How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.

+ 85 - 0
README

@@ -0,0 +1,85 @@
+odhcpd - Embedded DHCP/DHCPv6/RA Server & Relay
+
+** Abstract **
+
+odhcpd is a daemon for serving and relaying IP management protocols to
+configure clients and downstream routers. It tries to follow the RFC 6204
+requirements for IPv6 home routers.
+
+odhcpd provides server services for DHCP, RA, stateless and stateful DHCPv6,
+prefix delegation and can be used to relay RA, DHCPv6 and NDP between routed
+(non-bridged) interfaces in case no delegated prefixes are available.
+
+
+** Features **
+
+1. Router Discovery support (solicitations and advertisements) with 2 modes
+   server:	RD server for slave interfaces
+   a) automatic detection of prefixes, delegated prefix and default routes, MTU
+   b) automatic reannouncement when changes to prefixes or routes occur
+
+   relay:	RD relay between master and slave interfaces
+   a) support for rewriting announced DNS-server addresses in relay mode
+   
+3. DHCPv6-support with 2 modes of operation
+   server:	minimalistic server mode
+   a) stateless and stateful address assignment
+   b) prefix delegation support
+   c) dynamic reconfiguration in case prefixes change
+   d) hostname detection and hosts-file creation
+
+   relay: 	mostly standards-compliant DHCPv6-relay
+   a) support for rewriting announced DNS-server addresses
+   
+4. Proxy for Neighbor Discovery messages (solicitations and advertisments)
+   a) support for auto-learning routes to the local routing table
+   b) support for marking interfaces "external" not proxying NDP for them
+      and only serving NDP for DAD and for traffic to the router itself
+      [Warning: you should provide additional firewall rules for security]
+
+
+** Compiling **
+
+odhcpd uses cmake:
+* To prepare a Makefile use:  "cmake ." 
+* To build / install use: "make" / "make install" afterwards.
+* To build DEB or RPM packages use: "make package" afterwards.
+
+
+** Server Mode **
+
+0. Server mode is used as a minimalistic alternative for full-blown servers
+   like radvd or ISC DHCP if simplicity or a small footprint matter.
+   Note: The master interface is unused in this mode. It should be set to '.'.
+
+1. If there are non-local addresses assigned to the slave interface when a
+   router solicitation is received, said prefixes are announced automatically
+   for stateless autoconfiguration and also offered via stateful DHCPv6.
+   If all prefixes are bigger than /64 all but the first /64 of these prefixes
+   is offered via DHCPv6-PD to downstream routers.
+
+2. If DNS servers should be announced (DHCPv6 server-mode) then a local DNS-
+   proxy (e.g. dnsmasq) needs to be run on the router itself because 6relayd
+   will always announce a local address as DNS-server (if not otherwise
+   configured).
+
+3. odhcpd is run with the appropriate configuration.
+
+
+** Relay Mode **
+
+0. Relay mode is used when a /64-bit IPv6-Prefix should be distributed over
+   several links / isolated layer 2 domains (e.g. if no prefix delegation
+   is available). In this mode NDP (namely Router Discovery and Neighbor
+   Discovery) messages and DHCPv6-messages are proxied. For DHCPv6 also
+   server mode can be used instead of relaying if desired.
+
+1. When starting 6relayd it is required that the master interface - where
+   IPv6-service is already provided - is configured and usable.
+   
+2. If the upstream router doesn't provide or announce a DNS-service that is
+   reachable in an at least site-local scope, a local DNS proxy (e.g. dnsmasq)
+   needs to be run and configued on the same router where 6relayd is running.
+   (This step can most likely be skipped in most environments.)
+   
+3. odhcpd is run with the appropriate configuration.

+ 561 - 0
src/config.c

@@ -0,0 +1,561 @@
+#include <resolv.h>
+#include <signal.h>
+#include <arpa/inet.h>
+
+#include <uci.h>
+#include <uci_blob.h>
+
+#include "odhcpd.h"
+
+static struct blob_buf b;
+struct list_head leases = LIST_HEAD_INIT(leases);
+struct list_head interfaces = LIST_HEAD_INIT(interfaces);
+struct config config = {false, NULL, NULL};
+
+enum {
+	IFACE_ATTR_INTERFACE,
+	IFACE_ATTR_IFNAME,
+	IFACE_ATTR_DYNAMICDHCP,
+	IFACE_ATTR_IGNORE,
+	IFACE_ATTR_LEASETIME,
+	IFACE_ATTR_LIMIT,
+	IFACE_ATTR_START,
+	IFACE_ATTR_MASTER,
+	IFACE_ATTR_UPSTREAM,
+	IFACE_ATTR_RA,
+	IFACE_ATTR_DHCPV4,
+	IFACE_ATTR_DHCPV6,
+	IFACE_ATTR_NDPROXY,
+	IFACE_ATTR_DNS,
+	IFACE_ATTR_DOMAIN,
+	IFACE_ATTR_ULA_COMPAT,
+	IFACE_ATTR_RA_DEFAULT,
+	IFACE_ATTR_RA_MANAGEMENT,
+	IFACE_ATTR_RA_OFFLINK,
+	IFACE_ATTR_RA_PREFERENCE,
+	IFACE_ATTR_NDPROXY_ROUTING,
+	IFACE_ATTR_NDPROXY_SLAVE,
+	IFACE_ATTR_NDPROXY_STATIC,
+	IFACE_ATTR_MAX
+};
+
+static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = {
+	[IFACE_ATTR_INTERFACE] = { .name = "interface", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_IFNAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_DYNAMICDHCP] = { .name = "dynamicdhcp", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_IGNORE] = { .name = "ignore", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_LEASETIME] = { .name = "leasetime", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_START] = { .name = "start", .type = BLOBMSG_TYPE_INT32 },
+	[IFACE_ATTR_LIMIT] = { .name = "limit", .type = BLOBMSG_TYPE_INT32 },
+	[IFACE_ATTR_MASTER] = { .name = "master", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_UPSTREAM] = { .name = "upstream", .type = BLOBMSG_TYPE_ARRAY },
+	[IFACE_ATTR_RA] = { .name = "ra", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_DHCPV4] = { .name = "dhcpv4", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_DHCPV6] = { .name = "dhcpv6", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_NDPROXY] = { .name = "ndproxy", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_DNS] = { .name = "dns", .type = BLOBMSG_TYPE_ARRAY },
+	[IFACE_ATTR_DOMAIN] = { .name = "domain", .type = BLOBMSG_TYPE_ARRAY },
+	[IFACE_ATTR_ULA_COMPAT] = { .name = "ula_compat", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_RA_DEFAULT] = { .name = "ra_default", .type = BLOBMSG_TYPE_INT32 },
+	[IFACE_ATTR_RA_MANAGEMENT] = { .name = "ra_management", .type = BLOBMSG_TYPE_INT32 },
+	[IFACE_ATTR_RA_OFFLINK] = { .name = "ra_offlink", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_RA_PREFERENCE] = { .name = "ra_preference", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_NDPROXY_ROUTING] = { .name = "ndproxy_routing", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_NDPROXY_SLAVE] = { .name = "ndproxy_slave", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_NDPROXY_STATIC] = { .name = "ndproxy_static", .type = BLOBMSG_TYPE_ARRAY },
+};
+
+static const struct uci_blob_param_info iface_attr_info[IFACE_ATTR_MAX] = {
+	[IFACE_ATTR_UPSTREAM] = { .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_DNS] = { .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_DOMAIN] = { .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_NDPROXY_STATIC] = { .type = BLOBMSG_TYPE_STRING },
+};
+
+const struct uci_blob_param_list interface_attr_list = {
+	.n_params = IFACE_ATTR_MAX,
+	.params = iface_attrs,
+	.info = iface_attr_info,
+};
+
+
+enum {
+	LEASE_ATTR_IP,
+	LEASE_ATTR_MAC,
+	LEASE_ATTR_DUID,
+	LEASE_ATTR_HOSTID,
+	LEASE_ATTR_HOSTNAME,
+	LEASE_ATTR_MAX
+};
+
+
+static const struct blobmsg_policy lease_attrs[LEASE_ATTR_MAX] = {
+	[LEASE_ATTR_IP] = { .name = "ip", .type = BLOBMSG_TYPE_STRING },
+	[LEASE_ATTR_MAC] = { .name = "mac", .type = BLOBMSG_TYPE_STRING },
+	[LEASE_ATTR_DUID] = { .name = "duid", .type = BLOBMSG_TYPE_STRING },
+	[LEASE_ATTR_HOSTID] = { .name = "hostid", .type = BLOBMSG_TYPE_STRING },
+	[LEASE_ATTR_HOSTNAME] = { .name = "hostname", .type = BLOBMSG_TYPE_STRING },
+};
+
+
+const struct uci_blob_param_list lease_attr_list = {
+	.n_params = LEASE_ATTR_MAX,
+	.params = lease_attrs,
+};
+
+
+enum {
+	ODHCPD_ATTR_LEGACY,
+	ODHCPD_ATTR_LEASEFILE,
+	ODHCPD_ATTR_LEASETRIGGER,
+	ODHCPD_ATTR_MAX
+};
+
+
+static const struct blobmsg_policy odhcpd_attrs[LEASE_ATTR_MAX] = {
+	[ODHCPD_ATTR_LEGACY] = { .name = "legacy", .type = BLOBMSG_TYPE_BOOL },
+	[ODHCPD_ATTR_LEASEFILE] = { .name = "leasefile", .type = BLOBMSG_TYPE_STRING },
+	[ODHCPD_ATTR_LEASETRIGGER] = { .name = "leasetrigger", .type = BLOBMSG_TYPE_STRING },
+};
+
+
+const struct uci_blob_param_list odhcpd_attr_list = {
+	.n_params = ODHCPD_ATTR_MAX,
+	.params = odhcpd_attrs,
+};
+
+
+static struct interface* get_interface(const char *name)
+{
+	struct interface *c;
+	list_for_each_entry(c, &interfaces, head)
+		if (!strcmp(c->name, name))
+			return c;
+	return NULL;
+}
+
+
+static void clean_interface(struct interface *iface)
+{
+	free(iface->dns);
+	free(iface->search);
+	free(iface->upstream);
+	free(iface->static_ndp);
+	free(iface->dhcpv4_dns);
+	memset(&iface->ra, 0, sizeof(*iface) - offsetof(struct interface, ra));
+}
+
+
+static void close_interface(struct interface *iface)
+{
+	if (iface->head.next)
+		list_del(&iface->head);
+
+	setup_router_interface(iface, false);
+	setup_dhcpv6_interface(iface, false);
+	setup_ndp_interface(iface, false);
+	setup_dhcpv4_interface(iface, false);
+
+	clean_interface(iface);
+	free(iface);
+}
+
+
+static int parse_mode(const char *mode)
+{
+	if (!strcmp(mode, "disabled")) {
+		return RELAYD_DISABLED;
+	} else if (!strcmp(mode, "server")) {
+		return RELAYD_SERVER;
+	} else if (!strcmp(mode, "relay")) {
+		return RELAYD_RELAY;
+	} else if (!strcmp(mode, "hybrid")) {
+		return RELAYD_HYBRID;
+	} else {
+		return -1;
+	}
+}
+
+
+static void set_config(struct uci_section *s)
+{
+	struct blob_attr *tb[ODHCPD_ATTR_MAX], *c;
+
+	blob_buf_init(&b, 0);
+	uci_to_blob(&b, s, &lease_attr_list);
+	blobmsg_parse(lease_attrs, ODHCPD_ATTR_MAX, tb, blob_data(b.head), blob_len(b.head));
+
+	if ((c = tb[ODHCPD_ATTR_LEGACY]))
+		config.legacy = blobmsg_get_bool(c);
+
+	if ((c = tb[ODHCPD_ATTR_LEASEFILE])) {
+		free(config.dhcp_statefile);
+		config.dhcp_statefile = strdup(blobmsg_get_string(c));
+	}
+
+	if ((c = tb[ODHCPD_ATTR_LEASETRIGGER])) {
+		free(config.dhcp_cb);
+		config.dhcp_cb = strdup(blobmsg_get_string(c));
+	}
+}
+
+
+static int set_lease(struct uci_section *s)
+{
+	struct blob_attr *tb[LEASE_ATTR_MAX], *c;
+
+	blob_buf_init(&b, 0);
+	uci_to_blob(&b, s, &lease_attr_list);
+	blobmsg_parse(lease_attrs, LEASE_ATTR_MAX, tb, blob_data(b.head), blob_len(b.head));
+
+	size_t hostlen = 1;
+	if ((c = tb[LEASE_ATTR_HOSTNAME]))
+		hostlen = blobmsg_data_len(c);
+
+	struct lease *lease = calloc(1, sizeof(*lease) + hostlen);
+
+	if (hostlen > 1)
+		memcpy(lease->hostname, blobmsg_get_string(c), hostlen);
+
+	if ((c = tb[LEASE_ATTR_IP]))
+		if (inet_pton(AF_INET, blobmsg_get_string(c), &lease->ipaddr) < 0)
+			goto err;
+
+	if ((c = tb[LEASE_ATTR_MAC]))
+		if (!ether_aton_r(blobmsg_get_string(c), &lease->mac))
+			goto err;
+
+	if ((c = tb[LEASE_ATTR_DUID])) {
+		size_t duidlen = (blobmsg_data_len(c) - 1) / 2;
+		lease->duid = malloc(duidlen);
+		ssize_t len = odhcpd_unhexlify(lease->duid,
+				duidlen, blobmsg_get_string(c));
+
+		if (len < 0)
+			goto err;
+
+		lease->duid_len = len;
+	}
+
+	if ((c = tb[LEASE_ATTR_HOSTID]))
+		if (odhcpd_unhexlify((uint8_t*)&lease->hostid, sizeof(lease->hostid),
+				blobmsg_get_string(c)) < 0)
+			goto err;
+
+	list_add(&lease->head, &leases);
+	return 0;
+
+err:
+	free(lease->duid);
+	free(lease);
+	return -1;
+}
+
+
+int config_parse_interface(struct blob_attr *b, const char *name)
+{
+	bool overwrite = !!name;
+	struct blob_attr *tb[IFACE_ATTR_MAX], *c;
+	blobmsg_parse(iface_attrs, IFACE_ATTR_MAX, tb, blob_data(b), blob_len(b));
+
+	if (tb[IFACE_ATTR_INTERFACE])
+		name = blobmsg_data(tb[IFACE_ATTR_INTERFACE]);
+
+	struct interface *iface = get_interface(name);
+	if (!iface) {
+		iface = calloc(1, sizeof(*iface));
+		strncpy(iface->name, name, sizeof(iface->name) - 1);
+		list_add(&iface->head, &interfaces);
+	} else {
+		clean_interface(iface);
+	}
+
+	const char *ifname = NULL;
+#ifdef WITH_UBUS
+	if (overwrite)
+		ifname = ubus_get_ifname(name);
+#endif
+	if ((c = tb[IFACE_ATTR_IFNAME]))
+		ifname = blobmsg_get_string(c);
+
+	strncpy(iface->ifname, ifname, sizeof(iface->ifname) - 1);
+	iface->inuse = true;
+
+	if (overwrite)
+		clean_interface(iface);
+
+	if ((c = tb[IFACE_ATTR_DYNAMICDHCP]))
+		iface->no_dynamic_dhcp = !blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_IGNORE]))
+		iface->ignore = blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_LEASETIME])) {
+		char *val = blobmsg_get_string(c), *endptr;
+		double time = strtod(val, &endptr);
+		if (time && endptr[0]) {
+			if (endptr[0] == 's')
+				time *= 1;
+			else if (endptr[0] == 'm')
+				time *= 60;
+			else if (endptr[0] == 'h')
+				time *= 3600;
+			else if (endptr[0] == 'd')
+				time *= 24 * 3600;
+			else if (endptr[0] == 'w')
+				time *= 7 * 24 * 3600;
+			else
+				goto err;
+		}
+
+		if (time >= 60)
+			iface->dhcpv4_leasetime = time;
+	}
+
+	if ((c = tb[IFACE_ATTR_START])) {
+		iface->dhcpv4_start.s_addr = htonl(blobmsg_get_u32(c));
+
+		if (config.legacy)
+			iface->dhcpv4 = RELAYD_SERVER;
+	}
+
+	if ((c = tb[IFACE_ATTR_LIMIT]))
+		iface->dhcpv4_end.s_addr = htonl(
+				ntohl(iface->dhcpv4_start.s_addr) + blobmsg_get_u32(c));
+
+	if ((c = tb[IFACE_ATTR_MASTER]))
+		iface->master = blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_UPSTREAM])) {
+		struct blob_attr *cur;
+		int rem;
+
+		blobmsg_for_each_attr(cur, c, rem) {
+			if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL))
+				continue;
+
+			iface->upstream = realloc(iface->upstream,
+					iface->upstream_len + blobmsg_data_len(cur));
+			memcpy(iface->upstream + iface->upstream_len, blobmsg_get_string(cur), blobmsg_data_len(cur));
+			iface->upstream_len += blobmsg_data_len(cur);
+		}
+	}
+
+	if ((c = tb[IFACE_ATTR_RA]))
+		if ((iface->ra = parse_mode(blobmsg_get_string(c))) < 0)
+			goto err;
+
+	if ((c = tb[IFACE_ATTR_DHCPV4]))
+		if ((iface->dhcpv4 = parse_mode(blobmsg_get_string(c))) < 0)
+			goto err;
+
+	if ((c = tb[IFACE_ATTR_DHCPV6]))
+		if ((iface->dhcpv6 = parse_mode(blobmsg_get_string(c))) < 0)
+			goto err;
+
+	if ((c = tb[IFACE_ATTR_NDPROXY]))
+		iface->ndp = blobmsg_get_bool(c) ? RELAYD_RELAY : RELAYD_DISABLED;
+
+	if ((c = tb[IFACE_ATTR_DNS])) {
+		struct blob_attr *cur;
+		int rem;
+
+		blobmsg_for_each_attr(cur, c, rem) {
+			if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL))
+				continue;
+
+			struct in_addr addr4;
+			struct in6_addr addr6;
+			if (inet_pton(AF_INET, blobmsg_get_string(cur), &addr4) == 1) {
+				iface->dhcpv4_dns = realloc(iface->dhcpv4_dns,
+						(++iface->dhcpv4_dns_cnt) * sizeof(*iface->dhcpv4_dns));
+				iface->dhcpv4_dns[iface->dhcpv4_dns_cnt - 1] = addr4;
+			} else if (inet_pton(AF_INET6, blobmsg_get_string(cur), &addr6) == 1) {
+				iface->dns = realloc(iface->dns,
+						(++iface->dns_cnt) * sizeof(*iface->dns));
+				iface->dns[iface->dns_cnt - 1] = addr6;
+			} else {
+				goto err;
+			}
+		}
+	}
+
+	if ((c = tb[IFACE_ATTR_DOMAIN])) {
+		struct blob_attr *cur;
+		int rem;
+
+		blobmsg_for_each_attr(cur, c, rem) {
+			if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL))
+				continue;
+
+			uint8_t buf[256];
+			int len = dn_comp(blobmsg_get_string(cur), buf, sizeof(buf), NULL, NULL);
+			if (len <= 0)
+				goto err;
+
+			iface->search = realloc(iface->search, iface->search_len + len);
+			memcpy(&iface->search[iface->search_len], buf, len);
+			iface->search_len += len;
+		}
+	}
+
+	if ((c = tb[IFACE_ATTR_ULA_COMPAT]))
+		iface->deprecate_ula_if_public_avail = blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_RA_DEFAULT]))
+		iface->default_router = blobmsg_get_u32(c);
+
+	if ((c = tb[IFACE_ATTR_RA_MANAGEMENT]))
+		iface->managed = blobmsg_get_u32(c);
+
+	if ((c = tb[IFACE_ATTR_RA_OFFLINK]))
+		iface->ra_not_onlink = blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_RA_PREFERENCE])) {
+		const char *prio = blobmsg_get_string(c);
+
+		if (!strcmp(prio, "high"))
+			iface->route_preference = 1;
+		else if (!strcmp(prio, "low"))
+			iface->route_preference = -1;
+		else if (!strcmp(prio, "medium") || !strcmp(prio, "default"))
+			iface->route_preference = 0;
+		else
+			goto err;
+	}
+
+	if ((c = tb[IFACE_ATTR_NDPROXY_ROUTING]))
+		iface->learn_routes = blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_NDPROXY_SLAVE]))
+		iface->external = blobmsg_get_bool(c);
+
+	if ((c = tb[IFACE_ATTR_NDPROXY_STATIC])) {
+		struct blob_attr *cur;
+		int rem;
+
+		blobmsg_for_each_attr(cur, c, rem) {
+			if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, NULL))
+				continue;
+
+			int len = blobmsg_data_len(cur);
+			iface->static_ndp = realloc(iface->static_ndp, iface->static_ndp_len + len);
+			memcpy(&iface->static_ndp[iface->static_ndp_len], blobmsg_get_string(cur), len);
+			iface->static_ndp_len += len;
+		}
+	}
+
+	iface->ignore = (iface->ifindex = if_nametoindex(iface->ifname)) < 0;
+	return 0;
+
+err:
+	close_interface(iface);
+	return -1;
+}
+
+static int set_interface(struct uci_section *s)
+{
+	blob_buf_init(&b, 0);
+	uci_to_blob(&b, s, &interface_attr_list);
+	return config_parse_interface(b.head, s->e.name);
+}
+
+
+static volatile bool do_reload = false;
+static void set_stop(int signal)
+{
+	uloop_end();
+	do_reload = (signal == SIGHUP);
+}
+
+void odhcpd_run(void)
+{
+	struct uci_context *uci = uci_alloc_context();
+	signal(SIGTERM, set_stop);
+	signal(SIGHUP, set_stop);
+	signal(SIGINT, set_stop);
+
+	do {
+		do_reload = false;
+
+		struct lease *l;
+		list_for_each_entry(l, &leases, head) {
+			list_del(&l->head);
+			free(l->duid);
+			free(l);
+		}
+
+		struct uci_package *dhcp = NULL;
+		if (!uci_load(uci, "dhcp", &dhcp)) {
+			struct uci_element *e;
+			uci_foreach_element(&dhcp->sections, e) {
+				struct uci_section *s = uci_to_section(e);
+				if (!strcmp(s->type, "lease"))
+					set_lease(s);
+				else if (!strcmp(s->type, "odhcpd"))
+					set_config(s);
+			}
+
+			uci_foreach_element(&dhcp->sections, e) {
+				struct uci_section *s = uci_to_section(e);
+				if (!strcmp(s->type, "dhcp"))
+					set_interface(s);
+			}
+		}
+
+#ifdef WITH_UBUS
+		ubus_apply_network();
+#endif
+
+		// Evaluate hybrid mode for master
+		struct interface *master = NULL, *i;
+		list_for_each_entry(i, &interfaces, head) {
+			if (!i->master)
+				continue;
+
+			enum odhcpd_mode hybrid_mode = RELAYD_DISABLED;
+			if (i->dhcpv6 == RELAYD_HYBRID)
+				i->dhcpv6 = hybrid_mode;
+
+			if (i->ra == RELAYD_HYBRID)
+				i->ra = hybrid_mode;
+
+			if (i->ndp == RELAYD_HYBRID)
+				i->ndp = hybrid_mode;
+
+			if (i->dhcpv6 == RELAYD_RELAY || i->ra == RELAYD_RELAY || i->ndp == RELAYD_RELAY)
+				master = i;
+		}
+
+
+		list_for_each_entry(i, &interfaces, head) {
+			if (i->inuse && !i->ignore) {
+				// Resolve hybrid mode
+				if (i->dhcpv6 == RELAYD_HYBRID)
+					i->dhcpv6 = (master && master->dhcpv6 == RELAYD_RELAY) ?
+							RELAYD_RELAY : RELAYD_SERVER;
+
+				if (i->ra == RELAYD_HYBRID)
+					i->ra = (master && master->ra == RELAYD_RELAY) ?
+							RELAYD_RELAY : RELAYD_SERVER;
+
+				if (i->ndp == RELAYD_HYBRID)
+					i->ndp = (master && master->ndp == RELAYD_RELAY) ?
+							RELAYD_RELAY : RELAYD_SERVER;
+
+				setup_router_interface(i, true);
+				setup_dhcpv6_interface(i, true);
+				setup_ndp_interface(i, true);
+				setup_dhcpv4_interface(i, true);
+			} else {
+				close_interface(i);
+			}
+		}
+
+		uloop_run();
+
+		if (dhcp)
+			uci_unload(uci, dhcp);
+	} while (do_reload);
+}
+

+ 508 - 0
src/dhcpv4.c

@@ -0,0 +1,508 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ *
+ */
+
+#include <time.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <resolv.h>
+#include <net/if.h>
+#include <net/if_arp.h>
+#include <netinet/ip.h>
+#include <sys/ioctl.h>
+#include <sys/timerfd.h>
+#include <arpa/inet.h>
+
+#include "odhcpd.h"
+#include "dhcpv4.h"
+#include "dhcpv6.h"
+
+
+static void handle_dhcpv4(void *addr, void *data, size_t len,
+		struct interface *iface);
+static struct dhcpv4_assignment* dhcpv4_lease(struct interface *iface,
+		enum dhcpv4_msg msg, const uint8_t *mac, struct in_addr reqaddr,
+		const char *hostname);
+
+
+// Create socket and register events
+int init_dhcpv4(void)
+{
+	return 0;
+}
+
+
+int setup_dhcpv4_interface(struct interface *iface, bool enable)
+{
+	if (iface->dhcpv4_event.uloop.fd > 0) {
+		close(iface->dhcpv4_event.uloop.fd);
+		iface->dhcpv4_event.uloop.fd = -1;
+	}
+
+	if (iface->dhcpv4 && enable) {
+		if (!iface->dhcpv4_assignments.next)
+			INIT_LIST_HEAD(&iface->dhcpv4_assignments);
+
+		int sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP);
+
+		// Basic IPv6 configuration
+		int val = 1;
+		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
+		setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val));
+		setsockopt(sock, IPPROTO_IP, IP_PKTINFO, &val, sizeof(val));
+
+		val = IPTOS_CLASS_CS6;
+		setsockopt(sock, IPPROTO_IP, IP_TOS, &val, sizeof(val));
+
+		val = IP_PMTUDISC_DONT;
+		setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val));
+
+		setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE,
+				iface->ifname, strlen(iface->ifname));
+
+		struct sockaddr_in bind_addr = {AF_INET, htons(DHCPV4_SERVER_PORT),
+					{INADDR_ANY}, {0}};
+
+		if (bind(sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr))) {
+			syslog(LOG_ERR, "Failed to open DHCPv4 server socket: %s",
+					strerror(errno));
+			return -1;
+		}
+
+
+		if (ntohl(iface->dhcpv4_start.s_addr) > ntohl(iface->dhcpv4_end.s_addr)) {
+			syslog(LOG_ERR, "Invalid DHCP range");
+			return -1;
+		}
+
+		// Create a range if not specified
+		struct ifreq ifreq;
+		strncpy(ifreq.ifr_name, iface->ifname, sizeof(ifreq.ifr_name));
+
+		struct sockaddr_in *saddr = (struct sockaddr_in*)&ifreq.ifr_addr;
+		struct sockaddr_in *smask = (struct sockaddr_in*)&ifreq.ifr_netmask;
+		if (!(iface->dhcpv4_start.s_addr & htonl(0xffff0000)) &&
+				!(iface->dhcpv4_end.s_addr & htonl(0xffff0000)) &&
+				!ioctl(sock, SIOCGIFADDR, &ifreq)) {
+			struct in_addr addr = saddr->sin_addr;
+
+			ioctl(sock, SIOCGIFNETMASK, &ifreq);
+			struct in_addr mask = smask->sin_addr;
+
+			uint32_t start = ntohl(iface->dhcpv4_start.s_addr);
+			uint32_t end = ntohl(iface->dhcpv4_end.s_addr);
+
+			if (start && end && start < end &&
+					start > ntohl(addr.s_addr & ~mask.s_addr) &&
+					(start & ntohl(mask.s_addr)) == start &&
+					(end & ntohl(mask.s_addr)) == end) {
+				iface->dhcpv4_start.s_addr = htonl(start) |
+						(addr.s_addr & mask.s_addr);
+				iface->dhcpv4_end.s_addr = htonl(end) |
+						(addr.s_addr & mask.s_addr);
+			} else if (ntohl(mask.s_addr) <= 0xffffffc0) {
+				start = addr.s_addr & mask.s_addr;
+				end = addr.s_addr & mask.s_addr;
+
+				if (ntohl(mask.s_addr) <= 0xffffff00) {
+					iface->dhcpv4_start.s_addr = start | htonl(20);
+					iface->dhcpv4_end.s_addr = end | htonl(199);
+				} else {
+					iface->dhcpv4_start.s_addr = start | htonl(10);
+					iface->dhcpv4_end.s_addr = end | htonl(59);
+				}
+			}
+
+
+		}
+
+		// Parse static entries
+		struct lease *lease;
+		list_for_each_entry(lease, &leases, head) {
+			// Construct entry
+			size_t hostlen = strlen(lease->hostname) + 1;
+			struct dhcpv4_assignment *a = calloc(1, sizeof(*a) + hostlen);
+
+			a->addr = ntohl(lease->ipaddr.s_addr);
+			memcpy(a->hwaddr, lease->mac.ether_addr_octet, sizeof(a->hwaddr));
+			memcpy(a->hostname, lease->hostname, hostlen);
+
+			// Assign to all interfaces
+			struct dhcpv4_assignment *c;
+			list_for_each_entry(c, &iface->dhcpv4_assignments, head) {
+				if (c->addr > a->addr) {
+					list_add_tail(&a->head, &c->head);
+				} else if (c->addr == a->addr) {
+					// Already an assignment with that number
+					break;
+				}
+			}
+
+			if (!a->head.next)
+				free(a);
+		}
+
+		// Clean invalid assignments
+		struct dhcpv4_assignment *a, *n;
+		list_for_each_entry_safe(a, n, &iface->dhcpv4_assignments, head) {
+			if ((htonl(a->addr) & smask->sin_addr.s_addr) !=
+					(saddr->sin_addr.s_addr & smask->sin_addr.s_addr)) {
+				list_del(&a->head);
+				free(a);
+			}
+		}
+
+
+		if (iface->dhcpv4_leasetime < 60)
+			iface->dhcpv4_leasetime = 1800;
+
+		iface->dhcpv4_event.uloop.fd = sock;
+		iface->dhcpv4_event.handle_dgram = handle_dhcpv4;
+		odhcpd_register(&iface->dhcpv4_event);
+	} else if (iface->dhcpv4_assignments.next) {
+		while (!list_empty(&iface->dhcpv4_assignments)) {
+			struct dhcpv4_assignment *a = list_first_entry(&iface->dhcpv4_assignments,
+					struct dhcpv4_assignment, head);
+			list_del(&a->head);
+			free(a->hostname);
+			free(a);
+		}
+
+	}
+	return 0;
+}
+
+
+static void dhcpv4_put(struct dhcpv4_message *msg, uint8_t **cookie,
+		uint8_t type, uint8_t len, const void *data)
+{
+	uint8_t *c = *cookie;
+	if (*cookie + 2 + len > (uint8_t*)&msg[1])
+		return;
+
+	*c++ = type;
+	*c++ = len;
+	memcpy(c, data, len);
+
+	*cookie = c + len;
+}
+
+
+// Simple DHCPv6-server for information requests
+static void handle_dhcpv4(void *addr, void *data, size_t len,
+		struct interface *iface)
+{
+	if (!iface->dhcpv4)
+		return;
+
+	struct dhcpv4_message *req = data;
+	if (len < offsetof(struct dhcpv4_message, options) + 4 ||
+			req->op != DHCPV4_BOOTREQUEST || req->hlen != 6)
+		return;
+
+	int sock = iface->dhcpv4_event.uloop.fd;
+	struct sockaddr_in ifaddr;
+	struct sockaddr_in ifnetmask;
+
+	syslog(LOG_NOTICE, "Got DHCPv4 request");
+
+	struct ifreq ifreq;
+	memcpy(ifreq.ifr_name, iface->ifname, sizeof(ifreq.ifr_name));
+	if (ioctl(sock, SIOCGIFADDR, &ifreq)) {
+		syslog(LOG_WARNING, "DHCPv4 failed to detect address: %s", strerror(errno));
+		return;
+	}
+
+	memcpy(&ifaddr, &ifreq.ifr_addr, sizeof(ifaddr));
+	if (ioctl(sock, SIOCGIFNETMASK, &ifreq))
+		return;
+
+	memcpy(&ifnetmask, &ifreq.ifr_netmask, sizeof(ifnetmask));
+	uint32_t network = ifaddr.sin_addr.s_addr & ifnetmask.sin_addr.s_addr;
+
+	if ((iface->dhcpv4_start.s_addr & ifnetmask.sin_addr.s_addr) != network ||
+			(iface->dhcpv4_end.s_addr & ifnetmask.sin_addr.s_addr) != network) {
+		syslog(LOG_WARNING, "DHCPv4 range out of assigned network");
+		return;
+	}
+
+	struct ifreq ifr = {.ifr_name = ""};
+	strncpy(ifr.ifr_name, iface->ifname, sizeof(ifr.ifr_name));
+
+	struct dhcpv4_message reply = {
+		.op = DHCPV4_BOOTREPLY,
+		.htype = 1,
+		.hlen = 6,
+		.hops = 0,
+		.xid = req->xid,
+		.secs = 0,
+		.flags = req->flags,
+		.ciaddr = {INADDR_ANY},
+		.giaddr = req->giaddr,
+		.siaddr = ifaddr.sin_addr,
+	};
+	memcpy(reply.chaddr, req->chaddr, sizeof(reply.chaddr));
+
+	reply.options[0] = 0x63;
+	reply.options[1] = 0x82;
+	reply.options[2] = 0x53;
+	reply.options[3] = 0x63;
+
+	uint8_t *cookie = &reply.options[4];
+	uint8_t reqmsg = DHCPV4_MSG_REQUEST;
+	uint8_t msg = DHCPV4_MSG_ACK;
+
+	struct in_addr reqaddr = {INADDR_ANY};
+	char hostname[256];
+	hostname[0] = 0;
+
+	uint8_t *start = &req->options[4];
+	uint8_t *end = ((uint8_t*)data) + len;
+	struct dhcpv4_option *opt;
+	dhcpv4_for_each_option(start, end, opt) {
+		if (opt->type == DHCPV4_OPT_MESSAGE && opt->len == 1) {
+			reqmsg = opt->data[0];
+		} else if (opt->type == DHCPV4_OPT_HOSTNAME && opt->len > 0) {
+			memcpy(hostname, opt->data, opt->len);
+			hostname[opt->len] = 0;
+		} else if (opt->type == DHCPV4_OPT_IPADDRESS && opt->len == 4) {
+			memcpy(&reqaddr, opt->data, 4);
+		} else if (opt->type == DHCPV4_OPT_SERVERID && opt->len == 4) {
+			if (memcmp(opt->data, &ifaddr.sin_addr, 4))
+				return;
+		}
+	}
+
+	if (reqmsg != DHCPV4_MSG_DISCOVER && reqmsg != DHCPV4_MSG_REQUEST &&
+			reqmsg != DHCPV4_MSG_INFORM && reqmsg != DHCPV4_MSG_DECLINE &&
+			reqmsg != DHCPV4_MSG_RELEASE)
+		return;
+
+	struct dhcpv4_assignment *lease = NULL;
+	if (reqmsg != DHCPV4_MSG_INFORM)
+		lease = dhcpv4_lease(iface, reqmsg, req->chaddr, reqaddr, hostname);
+
+	if (!lease) {
+		if (reqmsg == DHCPV4_MSG_REQUEST)
+			msg = DHCPV4_MSG_NAK;
+		else if (reqmsg == DHCPV4_MSG_DISCOVER)
+			return;
+	} else if (reqmsg == DHCPV4_MSG_DISCOVER) {
+		msg = DHCPV4_MSG_OFFER;
+	}
+
+	if (reqmsg == DHCPV4_MSG_DECLINE || reqmsg == DHCPV4_MSG_RELEASE)
+		return;
+
+	dhcpv4_put(&reply, &cookie, DHCPV4_OPT_MESSAGE, 1, &msg);
+	dhcpv4_put(&reply, &cookie, DHCPV4_OPT_SERVERID, 4, &ifaddr.sin_addr);
+
+	if (lease) {
+		reply.yiaddr.s_addr = htonl(lease->addr);
+
+		uint32_t val = htonl(iface->dhcpv4_leasetime);
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_LEASETIME, 4, &val);
+
+		val = htonl(500 * iface->dhcpv4_leasetime / 1000);
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_RENEW, 4, &val);
+
+		val = htonl(875 * iface->dhcpv4_leasetime / 1000);
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_REBIND, 4, &val);
+
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_NETMASK, 4, &ifnetmask.sin_addr);
+
+		if (lease->hostname[0])
+			dhcpv4_put(&reply, &cookie, DHCPV4_OPT_HOSTNAME,
+					strlen(lease->hostname), lease->hostname);
+
+		if (!ioctl(sock, SIOCGIFBRDADDR, &ifr)) {
+			struct sockaddr_in *ina = (struct sockaddr_in*)&ifr.ifr_broadaddr;
+			dhcpv4_put(&reply, &cookie, DHCPV4_OPT_BROADCAST, 4, &ina->sin_addr);
+		}
+	}
+
+	if (!ioctl(sock, SIOCGIFMTU, &ifr)) {
+		uint16_t mtu = htons(ifr.ifr_mtu);
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_MTU, 2, &mtu);
+	}
+
+	if (iface->search) {
+		char b[256];
+		if (dn_expand(iface->search, iface->search + iface->search_len,
+				iface->search, b, sizeof(b)) > 0)
+			dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DOMAIN, strlen(b), b);
+	} else if (!res_init() && _res.dnsrch[0] && _res.dnsrch[0][0]) {
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DOMAIN,
+				strlen(_res.dnsrch[0]), _res.dnsrch[0]);
+	}
+
+	dhcpv4_put(&reply, &cookie, DHCPV4_OPT_ROUTER, 4, &ifaddr.sin_addr);
+
+
+
+	if (iface->dhcpv4_dns_cnt == 0)
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DNSSERVER, 4, &ifaddr.sin_addr);
+	else
+		dhcpv4_put(&reply, &cookie, DHCPV4_OPT_DNSSERVER,
+				4 * iface->dhcpv4_dns_cnt, iface->dhcpv4_dns);
+
+
+	dhcpv4_put(&reply, &cookie, DHCPV4_OPT_END, 0, NULL);
+
+	struct sockaddr_in dest = *((struct sockaddr_in*)addr);
+	if (req->giaddr.s_addr) {
+		dest.sin_addr = req->giaddr;
+		dest.sin_port = htons(DHCPV4_SERVER_PORT);
+	} else if (req->ciaddr.s_addr && req->ciaddr.s_addr != dest.sin_addr.s_addr) {
+		dest.sin_addr = req->ciaddr;
+		dest.sin_port = htons(DHCPV4_CLIENT_PORT);
+	} else if ((ntohs(req->flags) & DHCPV4_FLAG_BROADCAST) ||
+			req->hlen != reply.hlen) {
+		dest.sin_addr.s_addr = INADDR_BROADCAST;
+		dest.sin_port = htons(DHCPV4_CLIENT_PORT);
+	} else {
+		dest.sin_addr = reply.yiaddr;
+		dest.sin_port = htons(DHCPV4_CLIENT_PORT);
+
+		struct arpreq arp = {.arp_flags = ATF_COM};
+		memcpy(arp.arp_ha.sa_data, req->chaddr, 6);
+		memcpy(&arp.arp_pa, &dest, sizeof(arp.arp_pa));
+		memcpy(arp.arp_dev, iface->ifname, sizeof(arp.arp_dev));
+		ioctl(sock, SIOCSARP, &arp);
+	}
+
+	sendto(sock, &reply, sizeof(reply), MSG_DONTWAIT,
+			(struct sockaddr*)&dest, sizeof(dest));
+}
+
+
+static bool dhcpv4_assign(struct interface *iface,
+		struct dhcpv4_assignment *assign, uint32_t raddr)
+{
+	const unsigned tries = 10;
+	uint32_t start = ntohl(iface->dhcpv4_start.s_addr);
+	uint32_t end = ntohl(iface->dhcpv4_end.s_addr);
+	uint32_t count = end - start + 1;
+
+	// Seed RNG with checksum of DUID
+	uint32_t seed = 0;
+	for (size_t i = 0; i < sizeof(assign->hwaddr); ++i)
+		seed += assign->hwaddr[i];
+	srand(seed);
+
+	// Try to assign up to 100x
+	for (unsigned i = 0; i < tries; ++i) {
+		uint32_t try = (((uint32_t)rand()) % count) + start;
+		if (i == 0 && raddr >= start && raddr <= end)
+			try = raddr;
+		else if (i == tries - 1)
+			try = start;
+
+		if (list_empty(&iface->dhcpv4_assignments)) {
+			assign->addr = try;
+			list_add(&assign->head, &iface->dhcpv4_assignments);
+			return true;
+		}
+
+		struct dhcpv4_assignment *c;
+		list_for_each_entry(c, &iface->dhcpv4_assignments, head) {
+			if (c->addr > try) {
+				assign->addr = try;
+				list_add_tail(&assign->head, &c->head);
+				return true;
+			} else if (c->addr == try) {
+				if (i < tries - 1)
+					break;
+				else
+					++try;
+			}
+		}
+	}
+
+	return false;
+}
+
+
+static struct dhcpv4_assignment* dhcpv4_lease(struct interface *iface,
+		enum dhcpv4_msg msg, const uint8_t *mac, struct in_addr reqaddr,
+		const char *hostname)
+{
+	struct dhcpv4_assignment *lease = NULL;
+	uint32_t raddr = ntohl(reqaddr.s_addr);
+	time_t now = odhcpd_time();
+
+	struct dhcpv4_assignment *c, *n, *a = NULL;
+	list_for_each_entry_safe(c, n, &iface->dhcpv4_assignments, head) {
+		if (c->addr == raddr && !memcmp(c->hwaddr, mac, 6)) {
+			a = c;
+			break;
+		} else if (c->valid_until < now) {
+			list_del(&c->head);
+			free(c);
+		}
+	}
+
+	bool update_state = false;
+	if (msg == DHCPV4_MSG_DISCOVER || msg == DHCPV4_MSG_REQUEST) {
+		bool assigned = !!a;
+		size_t hostlen = strlen(hostname) + 1;
+
+		if (!a && !iface->no_dynamic_dhcp) { // Create new binding
+			a = calloc(1, sizeof(*a) + hostlen);
+			memcpy(a->hwaddr, mac, sizeof(a->hwaddr));
+			memcpy(a->hostname, hostname, hostlen);
+
+			assigned = dhcpv4_assign(iface, a, raddr);
+		}
+
+		if (assigned && !a->hostname[0] && hostname) {
+			a = realloc(a, sizeof(*a) + hostlen);
+			memcpy(a->hostname, hostname, hostlen);
+
+			// Fixup list
+			a->head.next->prev = &a->head;
+			a->head.prev->next = &a->head;
+		}
+
+		// Was only a solicitation: mark binding for removal
+		if (assigned && a->valid_until < now) {
+			a->valid_until = (msg == DHCPV4_MSG_DISCOVER) ? 0 :
+					(now + iface->dhcpv4_leasetime);
+		} else if (!assigned && a) { // Cleanup failed assignment
+			free(a);
+			a = NULL;
+		}
+
+		if (assigned && a)
+			lease = a;
+	} else if (msg == DHCPV4_MSG_RELEASE) {
+		if (a) {
+			a->valid_until = 0;
+			update_state = true;
+		}
+	} else if (msg == DHCPV4_MSG_DECLINE) {
+		memset(a->hwaddr, 0, sizeof(a->hwaddr));
+		a->valid_until = now + 3600; // Block address for 1h
+		update_state = true;
+	}
+
+	if (update_state)
+		dhcpv6_write_statefile();
+
+	return lease;
+}
+

+ 93 - 0
src/dhcpv4.h

@@ -0,0 +1,93 @@
+/**
+ *   Copyright (C) 2012 Steven Barth <steven@midlink.org>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2
+ *   as published by the Free Software Foundation.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License version 2 for more details.
+ *
+ */
+#pragma once
+
+#define DHCPV4_CLIENT_PORT 68
+#define DHCPV4_SERVER_PORT 67
+
+#define DHCPV4_FLAG_BROADCAST  0x8000
+
+enum dhcpv4_op {
+	DHCPV4_BOOTREQUEST = 1,
+	DHCPV4_BOOTREPLY = 2
+};
+
+enum dhcpv4_msg {
+	DHCPV4_MSG_DISCOVER = 1,
+	DHCPV4_MSG_OFFER = 2,
+	DHCPV4_MSG_REQUEST = 3,
+	DHCPV4_MSG_DECLINE = 4,
+	DHCPV4_MSG_ACK = 5,
+	DHCPV4_MSG_NAK = 6,
+	DHCPV4_MSG_RELEASE = 7,
+	DHCPV4_MSG_INFORM = 8,
+};
+
+enum dhcpv4_opt {
+	DHCPV4_OPT_NETMASK = 1,
+	DHCPV4_OPT_ROUTER = 3,
+	DHCPV4_OPT_DNSSERVER = 6,
+	DHCPV4_OPT_DOMAIN = 15,
+	DHCPV4_OPT_MTU = 26,
+	DHCPV4_OPT_BROADCAST = 28,
+	DHCPV4_OPT_NTPSERVER = 42,
+	DHCPV4_OPT_LEASETIME = 51,
+	DHCPV4_OPT_MESSAGE = 53,
+	DHCPV4_OPT_SERVERID = 54,
+	DHCPV4_OPT_RENEW = 58,
+	DHCPV4_OPT_REBIND = 59,
+	DHCPV4_OPT_IPADDRESS = 50,
+	DHCPV4_OPT_HOSTNAME = 10,
+	DHCPV4_OPT_REQUEST = 17,
+	DHCPV4_OPT_END = 255,
+};
+
+struct dhcpv4_message {
+	uint8_t op;
+	uint8_t htype;
+	uint8_t hlen;
+	uint8_t hops;
+	uint32_t xid;
+	uint16_t secs;
+	uint16_t flags;
+	struct in_addr ciaddr;
+	struct in_addr yiaddr;
+	struct in_addr siaddr;
+	struct in_addr giaddr;
+	uint8_t chaddr[16];
+	char sname[64];
+	char file[128];
+	uint8_t options[312];
+};
+
+struct dhcpv4_assignment {
+	struct list_head head;
+	uint32_t addr;
+	time_t valid_until;
+	uint8_t hwaddr[6];
+	char hostname[];
+};
+
+struct dhcpv4_option {
+	uint8_t type;
+	uint8_t len;
+	uint8_t data[];
+};
+
+
+#define dhcpv4_for_each_option(start, end, opt)\
+	for (opt = (struct dhcpv4_option*)(start); \
+		&opt[1] <= (struct dhcpv4_option*)(end) && \
+			&opt->data[opt->len] <= (end); \
+		opt = (struct dhcpv4_option*)&opt->data[opt->len])

+ 893 - 0
src/dhcpv6-ia.c

@@ -0,0 +1,893 @@
+/**
+ * Copyright (C) 2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#include "odhcpd.h"
+#include "dhcpv6.h"
+#include "dhcpv4.h"
+#include "md5.h"
+
+#include <time.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <alloca.h>
+#include <resolv.h>
+#include <limits.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <sys/timerfd.h>
+
+
+static void update(struct interface *iface);
+static void reconf_timer(struct uloop_timeout *event);
+static struct uloop_timeout reconf_event = {.cb = reconf_timer};
+static int socket_fd = -1;
+static uint32_t serial = 0;
+
+
+int dhcpv6_ia_init(int dhcpv6_socket)
+{
+	socket_fd = dhcpv6_socket;
+	uloop_timeout_set(&reconf_event, 2000);
+	return 0;
+}
+
+
+int setup_dhcpv6_ia_interface(struct interface *iface, bool enable)
+{
+	if (!enable && iface->ia_assignments.next) {
+		struct dhcpv6_assignment *c;
+		while (!list_empty(&iface->ia_assignments)) {
+			c = list_first_entry(&iface->ia_assignments, struct dhcpv6_assignment, head);
+			list_del(&c->head);
+			free(c);
+		}
+	}
+
+	if (iface->dhcpv6 == RELAYD_SERVER) {
+		if (!iface->ia_assignments.next)
+			INIT_LIST_HEAD(&iface->ia_assignments);
+
+		if (list_empty(&iface->ia_assignments)) {
+			struct dhcpv6_assignment *border = calloc(1, sizeof(*border));
+			border->length = 64;
+			list_add(&border->head, &iface->ia_assignments);
+		}
+
+		update(iface);
+
+		// Parse static entries
+		struct lease *lease;
+		list_for_each_entry(lease, &leases, head) {
+			// Construct entry
+			struct dhcpv6_assignment *a = calloc(1, sizeof(*a) + lease->duid_len);
+			a->clid_len = lease->duid_len;
+			a->length = 128;
+			a->assigned = lease->hostid;
+			odhcpd_urandom(a->key, sizeof(a->key));
+			memcpy(a->clid_data, lease->duid, a->clid_len);
+			memcpy(a->mac, lease->mac.ether_addr_octet, sizeof(a->mac));
+
+			// Assign to all interfaces
+			struct dhcpv6_assignment *c;
+			list_for_each_entry(c, &iface->ia_assignments, head) {
+				if (c->length != 128 || c->assigned > a->assigned) {
+					list_add_tail(&a->head, &c->head);
+				} else if (c->assigned == a->assigned) {
+					// Already an assignment with that number
+					break;
+				}
+			}
+
+			if (a->head.next) {
+				if (lease->hostname[0]) {
+					free(a->hostname);
+					a->hostname = strdup(lease->hostname);
+				}
+			} else {
+				free(a);
+			}
+		}
+	}
+	return 0;
+}
+
+
+static int send_reconf(struct interface *iface, struct dhcpv6_assignment *assign)
+{
+	struct {
+		struct dhcpv6_client_header hdr;
+		uint16_t srvid_type;
+		uint16_t srvid_len;
+		uint16_t duid_type;
+		uint16_t hardware_type;
+		uint8_t mac[6];
+		uint16_t msg_type;
+		uint16_t msg_len;
+		uint8_t msg_id;
+		struct dhcpv6_auth_reconfigure auth;
+		uint16_t clid_type;
+		uint16_t clid_len;
+		uint8_t clid_data[128];
+	} __attribute__((packed)) reconf_msg = {
+		.hdr = {DHCPV6_MSG_RECONFIGURE, {0, 0, 0}},
+		.srvid_type = htons(DHCPV6_OPT_SERVERID),
+		.srvid_len = htons(10),
+		.duid_type = htons(3),
+		.hardware_type = htons(1),
+		.msg_type = htons(DHCPV6_OPT_RECONF_MSG),
+		.msg_len = htons(1),
+		.msg_id = DHCPV6_MSG_RENEW,
+		.auth = {htons(DHCPV6_OPT_AUTH),
+				htons(sizeof(reconf_msg.auth) - 4), 3, 1, 0,
+				{htonl(time(NULL)), htonl(++serial)}, 2, {0}},
+		.clid_type = htons(DHCPV6_OPT_CLIENTID),
+		.clid_len = htons(assign->clid_len),
+		.clid_data = {0},
+	};
+
+	odhcpd_get_mac(iface, reconf_msg.mac);
+	memcpy(reconf_msg.clid_data, assign->clid_data, assign->clid_len);
+	struct iovec iov = {&reconf_msg, sizeof(reconf_msg) - 128 + assign->clid_len};
+
+	md5_ctx_t md5;
+	uint8_t secretbytes[16];
+	memcpy(secretbytes, assign->key, sizeof(secretbytes));
+
+	for (size_t i = 0; i < sizeof(secretbytes); ++i)
+		secretbytes[i] ^= 0x36;
+
+	md5_begin(&md5);
+	md5_hash(secretbytes, sizeof(secretbytes), &md5);
+	md5_hash(iov.iov_base, iov.iov_len, &md5);
+	md5_end(reconf_msg.auth.key, &md5);
+
+	for (size_t i = 0; i < sizeof(secretbytes); ++i) {
+		secretbytes[i] ^= 0x36;
+		secretbytes[i] ^= 0x5c;
+	}
+
+	md5_begin(&md5);
+	md5_hash(secretbytes, sizeof(secretbytes), &md5);
+	md5_hash(reconf_msg.auth.key, 16, &md5);
+	md5_end(reconf_msg.auth.key, &md5);
+
+	return odhcpd_send(socket_fd, &assign->peer, &iov, 1, iface);
+}
+
+
+void dhcpv6_write_statefile(void)
+{
+	if (config.dhcp_statefile) {
+		time_t now = odhcpd_time(), wall_time = time(NULL);
+		int fd = open(config.dhcp_statefile, O_CREAT | O_WRONLY | O_CLOEXEC, 0644);
+		if (fd < 0)
+			return;
+
+		lockf(fd, F_LOCK, 0);
+		ftruncate(fd, 0);
+
+		FILE *fp = fdopen(fd, "w");
+		if (!fp) {
+			close(fd);
+			return;
+		}
+
+		struct interface *iface;
+		list_for_each_entry(iface, &interfaces, head) {
+			if (iface->dhcpv6 != RELAYD_SERVER && iface->dhcpv4 != RELAYD_SERVER)
+				continue;
+
+			if (iface->dhcpv6 == RELAYD_SERVER) {
+				struct dhcpv6_assignment *c;
+				list_for_each_entry(c, &iface->ia_assignments, head) {
+					if (c->clid_len == 0)
+						continue;
+
+					char ipbuf[INET6_ADDRSTRLEN];
+					char leasebuf[512];
+					char duidbuf[264];
+					odhcpd_hexlify(duidbuf, c->clid_data, c->clid_len);
+
+					// iface DUID iaid hostname lifetime assigned length [addrs...]
+					int l = snprintf(leasebuf, sizeof(leasebuf), "# %s %s %x %s %u %x %u ",
+							iface->ifname, duidbuf, ntohl(c->iaid),
+							(c->hostname ? c->hostname : "-"),
+							(unsigned)(c->valid_until > now ?
+									(c->valid_until - now + wall_time) : 0),
+							c->assigned, (unsigned)c->length);
+
+					struct in6_addr addr;
+					for (size_t i = 0; i < iface->ia_addr_len; ++i) {
+						if (iface->ia_addr[i].prefix > 64)
+							continue;
+
+						addr = iface->ia_addr[i].addr;
+						if (c->length == 128)
+							addr.s6_addr32[3] = htonl(c->assigned);
+						else
+							addr.s6_addr32[1] |= htonl(c->assigned);
+						inet_ntop(AF_INET6, &addr, ipbuf, sizeof(ipbuf) - 1);
+
+						if (c->length == 128 && c->hostname && i == 0)
+							fprintf(fp, "%s\t%s\n", ipbuf, c->hostname);
+
+						l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/%hhu ", ipbuf, c->length);
+					}
+					leasebuf[l - 1] = '\n';
+					fwrite(leasebuf, 1, l, fp);
+				}
+			}
+
+			if (iface->dhcpv4 == RELAYD_SERVER) {
+				struct dhcpv4_assignment *c;
+				list_for_each_entry(c, &iface->dhcpv4_assignments, head) {
+					char ipbuf[INET6_ADDRSTRLEN];
+					char leasebuf[512];
+					char duidbuf[16];
+					odhcpd_hexlify(duidbuf, c->hwaddr, sizeof(c->hwaddr));
+
+					// iface DUID iaid hostname lifetime assigned length [addrs...]
+					int l = snprintf(leasebuf, sizeof(leasebuf), "# %s %s ipv4 %s %u %x 32 ",
+							iface->ifname, duidbuf,
+							(c->hostname ? c->hostname : "-"),
+							(unsigned)(c->valid_until > now ?
+									(c->valid_until - now + wall_time) : 0),
+							c->addr);
+
+					struct in_addr addr = {htonl(c->addr)};
+					inet_ntop(AF_INET, &addr, ipbuf, sizeof(ipbuf) - 1);
+
+					if (c->hostname[0])
+						fprintf(fp, "%s\t%s\n", ipbuf, c->hostname);
+
+					l += snprintf(leasebuf + l, sizeof(leasebuf) - l, "%s/32 ", ipbuf);
+					leasebuf[l - 1] = '\n';
+					fwrite(leasebuf, 1, l, fp);
+				}
+			}
+		}
+
+		fclose(fp);
+	}
+
+	if (config.dhcp_cb) {
+		char *argv[2] = {config.dhcp_cb, NULL};
+		if (!vfork()) {
+			execv(argv[0], argv);
+			_exit(128);
+		}
+	}
+}
+
+
+static void apply_lease(struct interface *iface, struct dhcpv6_assignment *a, bool add)
+{
+	if (a->length > 64)
+		return;
+
+	for (size_t i = 0; i < iface->ia_addr_len; ++i) {
+		struct in6_addr prefix = iface->ia_addr[i].addr;
+		prefix.s6_addr32[1] |= htonl(a->assigned);
+		odhcpd_setup_route(&prefix, a->length, iface, &a->peer.sin6_addr, add);
+	}
+}
+
+
+static bool assign_pd(struct interface *iface, struct dhcpv6_assignment *assign)
+{
+	struct dhcpv6_assignment *c;
+	if (iface->ia_addr_len < 1)
+		return false;
+
+	// Try honoring the hint first
+	uint32_t current = 1, asize = (1 << (64 - assign->length)) - 1;
+	if (assign->assigned) {
+		list_for_each_entry(c, &iface->ia_assignments, head) {
+			if (c->length == 128)
+				continue;
+
+			if (assign->assigned >= current && assign->assigned + asize < c->assigned) {
+				list_add_tail(&assign->head, &c->head);
+				apply_lease(iface, assign, true);
+				return true;
+			}
+
+			if (c->assigned != 0)
+				current = (c->assigned + (1 << (64 - c->length)));
+		}
+	}
+
+	// Fallback to a variable assignment
+	current = 1;
+	list_for_each_entry(c, &iface->ia_assignments, head) {
+		if (c->length == 128)
+			continue;
+
+		current = (current + asize) & (~asize);
+		if (current + asize < c->assigned) {
+			assign->assigned = current;
+			list_add_tail(&assign->head, &c->head);
+			apply_lease(iface, assign, true);
+			return true;
+		}
+
+		if (c->assigned != 0)
+			current = (c->assigned + (1 << (64 - c->length)));
+	}
+
+	return false;
+}
+
+
+static bool assign_na(struct interface *iface, struct dhcpv6_assignment *assign)
+{
+	if (iface->ia_addr_len < 1)
+		return false;
+
+	// Seed RNG with checksum of DUID
+	uint32_t seed = 0;
+	for (size_t i = 0; i < assign->clid_len; ++i)
+		seed += assign->clid_data[i];
+	srand(seed);
+
+	// Try to assign up to 100x
+	for (size_t i = 0; i < 100; ++i) {
+		uint32_t try;
+		do try = ((uint32_t)rand()) % 0x0fff; while (try < 0x100);
+
+		struct dhcpv6_assignment *c;
+		list_for_each_entry(c, &iface->ia_assignments, head) {
+			if (c->assigned > try || c->length != 128) {
+				assign->assigned = try;
+				list_add_tail(&assign->head, &c->head);
+				return true;
+			} else if (c->assigned == try) {
+				break;
+			}
+		}
+	}
+
+	return false;
+}
+
+
+static int prefixcmp(const void *va, const void *vb)
+{
+	const struct odhcpd_ipaddr *a = va, *b = vb;
+	uint32_t a_pref = ((a->addr.s6_addr[0] & 0xfe) != 0xfc) ? a->preferred : 1;
+	uint32_t b_pref = ((b->addr.s6_addr[0] & 0xfe) != 0xfc) ? b->preferred : 1;
+	return (a_pref < b_pref) ? 1 : (a_pref > b_pref) ? -1 : 0;
+}
+
+
+static void update(struct interface *iface)
+{
+	struct odhcpd_ipaddr addr[8];
+	memset(addr, 0, sizeof(addr));
+	int len = odhcpd_get_interface_addresses(iface->ifindex, addr, 8);
+
+	if (len < 0)
+		return;
+
+	qsort(addr, len, sizeof(*addr), prefixcmp);
+
+	time_t now = odhcpd_time();
+	int minprefix = -1;
+
+	for (int i = 0; i < len; ++i) {
+		if (addr[i].prefix > minprefix)
+			minprefix = addr[i].prefix;
+
+		addr[i].addr.s6_addr32[2] = 0;
+		addr[i].addr.s6_addr32[3] = 0;
+
+		if (addr[i].preferred < UINT32_MAX - now)
+			addr[i].preferred += now;
+
+		if (addr[i].valid < UINT32_MAX - now)
+			addr[i].valid += now;
+	}
+
+	struct dhcpv6_assignment *border = list_last_entry(&iface->ia_assignments, struct dhcpv6_assignment, head);
+	border->assigned = 1 << (64 - minprefix);
+
+	bool change = len != (int)iface->ia_addr_len;
+	for (int i = 0; !change && i < len; ++i)
+		if (addr[i].addr.s6_addr32[0] != iface->ia_addr[i].addr.s6_addr32[0] ||
+				addr[i].addr.s6_addr32[1] != iface->ia_addr[i].addr.s6_addr32[1] ||
+				(addr[i].preferred > 0) != (iface->ia_addr[i].preferred > 0) ||
+				(addr[i].valid > now + 7200) != (iface->ia_addr[i].valid > now + 7200))
+			change = true;
+
+	if (change) {
+		struct dhcpv6_assignment *c;
+		list_for_each_entry(c, &iface->ia_assignments, head)
+			if (c != border)
+				apply_lease(iface, c, false);
+	}
+
+	memcpy(iface->ia_addr, addr, len * sizeof(*addr));
+	iface->ia_addr_len = len;
+
+	if (change) { // Addresses / prefixes have changed
+		struct list_head reassign = LIST_HEAD_INIT(reassign);
+		struct dhcpv6_assignment *c, *d;
+		list_for_each_entry_safe(c, d, &iface->ia_assignments, head) {
+			if (c->clid_len == 0 || c->valid_until < now)
+				continue;
+
+			if (c->length < 128 && c->assigned >= border->assigned && c != border)
+				list_move(&c->head, &reassign);
+			else if (c != border)
+				apply_lease(iface, c, true);
+
+			if (c->accept_reconf && c->reconf_cnt == 0) {
+				c->reconf_cnt = 1;
+				c->reconf_sent = now;
+				send_reconf(iface, c);
+
+				// Leave all other assignments of that client alone
+				struct dhcpv6_assignment *a;
+				list_for_each_entry(a, &iface->ia_assignments, head)
+					if (a != c && a->clid_len == c->clid_len &&
+							!memcmp(a->clid_data, c->clid_data, a->clid_len))
+						c->reconf_cnt = INT_MAX;
+			}
+		}
+
+		while (!list_empty(&reassign)) {
+			c = list_first_entry(&reassign, struct dhcpv6_assignment, head);
+			list_del(&c->head);
+			if (!assign_pd(iface, c)) {
+				c->assigned = 0;
+				list_add(&c->head, &iface->ia_assignments);
+			}
+		}
+
+		dhcpv6_write_statefile();
+	}
+}
+
+
+static void reconf_timer(struct uloop_timeout *event)
+{
+	time_t now = odhcpd_time();
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head) {
+		if (iface->dhcpv6 != RELAYD_SERVER || iface->ia_assignments.next == NULL)
+			continue;
+
+		struct dhcpv6_assignment *a, *n;
+		list_for_each_entry_safe(a, n, &iface->ia_assignments, head) {
+			if (a->valid_until < now) {
+				if ((a->length < 128 && a->clid_len > 0) ||
+						(a->length == 128 && a->clid_len == 0)) {
+					list_del(&a->head);
+					free(a->hostname);
+					free(a);
+				}
+			} else if (a->reconf_cnt > 0 && a->reconf_cnt < 8 &&
+					now > a->reconf_sent + (1 << a->reconf_cnt)) {
+				++a->reconf_cnt;
+				a->reconf_sent = now;
+				send_reconf(iface, a);
+			}
+		}
+
+		if (iface->ia_reconf) {
+			update(iface);
+			iface->ia_reconf = false;
+		}
+	}
+
+	uloop_timeout_set(event, 2000);
+}
+
+
+static size_t append_reply(uint8_t *buf, size_t buflen, uint16_t status,
+		const struct dhcpv6_ia_hdr *ia, struct dhcpv6_assignment *a,
+		struct interface *iface, bool request)
+{
+	if (buflen < sizeof(*ia) + sizeof(struct dhcpv6_ia_prefix))
+		return 0;
+
+	struct dhcpv6_ia_hdr out = {ia->type, 0, ia->iaid, 0, 0};
+	size_t datalen = sizeof(out);
+	time_t now = odhcpd_time();
+
+	if (status) {
+		struct __attribute__((packed)) {
+			uint16_t type;
+			uint16_t len;
+			uint16_t value;
+		} stat = {htons(DHCPV6_OPT_STATUS), htons(sizeof(stat) - 4),
+				htons(status)};
+
+		memcpy(buf + datalen, &stat, sizeof(stat));
+		datalen += sizeof(stat);
+	} else {
+		if (a) {
+			uint32_t pref = 3600;
+			uint32_t valid = 3600;
+			bool have_non_ula = false;
+			for (size_t i = 0; i < iface->ia_addr_len; ++i)
+				if ((iface->ia_addr[i].addr.s6_addr[0] & 0xfe) != 0xfc)
+					have_non_ula = true;
+
+			for (size_t i = 0; i < iface->ia_addr_len; ++i) {
+				uint32_t prefix_pref = iface->ia_addr[i].preferred - now;
+				uint32_t prefix_valid = iface->ia_addr[i].valid - now;
+
+				if (iface->ia_addr[i].prefix > 64 ||
+						iface->ia_addr[i].preferred <= (uint32_t)now)
+					continue;
+
+				// ULA-deprecation compatibility workaround
+				if ((iface->ia_addr[i].addr.s6_addr[0] & 0xfe) == 0xfc &&
+						a->length == 128 && have_non_ula &&
+						iface->deprecate_ula_if_public_avail)
+					continue;
+
+				if (prefix_pref > 86400)
+					prefix_pref = 86400;
+
+				if (prefix_valid > 86400)
+					prefix_valid = 86400;
+
+				if (a->length < 128) {
+					struct dhcpv6_ia_prefix p = {
+						.type = htons(DHCPV6_OPT_IA_PREFIX),
+						.len = htons(sizeof(p) - 4),
+						.preferred = htonl(prefix_pref),
+						.valid = htonl(prefix_valid),
+						.prefix = a->length,
+						.addr = iface->ia_addr[i].addr
+					};
+					p.addr.s6_addr32[1] |= htonl(a->assigned);
+
+					if (datalen + sizeof(p) > buflen || a->assigned == 0)
+						continue;
+
+					memcpy(buf + datalen, &p, sizeof(p));
+					datalen += sizeof(p);
+				} else {
+					struct dhcpv6_ia_addr n = {
+						.type = htons(DHCPV6_OPT_IA_ADDR),
+						.len = htons(sizeof(n) - 4),
+						.addr = iface->ia_addr[i].addr,
+						.preferred = htonl(prefix_pref),
+						.valid = htonl(prefix_valid)
+					};
+					n.addr.s6_addr32[3] = htonl(a->assigned);
+
+					if (datalen + sizeof(n) > buflen || a->assigned == 0)
+						continue;
+
+					memcpy(buf + datalen, &n, sizeof(n));
+					datalen += sizeof(n);
+				}
+
+				// Calculate T1 / T2 based on non-deprecated addresses
+				if (prefix_pref > 0) {
+					if (prefix_pref < pref)
+						pref = prefix_pref;
+
+					if (prefix_valid < valid)
+						valid = prefix_valid;
+				}
+			}
+
+			a->valid_until = valid + now;
+			out.t1 = htonl(pref * 5 / 10);
+			out.t2 = htonl(pref * 8 / 10);
+
+			if (!out.t1)
+				out.t1 = htonl(1);
+
+			if (!out.t2)
+				out.t2 = htonl(1);
+		}
+
+		if (!request) {
+			uint8_t *odata, *end = ((uint8_t*)ia) + htons(ia->len) + 4;
+			uint16_t otype, olen;
+			dhcpv6_for_each_option((uint8_t*)&ia[1], end, otype, olen, odata) {
+				struct dhcpv6_ia_prefix *p = (struct dhcpv6_ia_prefix*)&odata[-4];
+				struct dhcpv6_ia_addr *n = (struct dhcpv6_ia_addr*)&odata[-4];
+				if ((otype != DHCPV6_OPT_IA_PREFIX || olen < sizeof(*p) - 4) &&
+						(otype != DHCPV6_OPT_IA_ADDR || olen < sizeof(*n) - 4))
+					continue;
+
+				bool found = false;
+				if (a) {
+					for (size_t i = 0; i < iface->ia_addr_len; ++i) {
+						if (iface->ia_addr[i].prefix > 64 ||
+								iface->ia_addr[i].preferred <= (uint32_t)now)
+							continue;
+
+						struct in6_addr addr = iface->ia_addr[i].addr;
+						if (ia->type == htons(DHCPV6_OPT_IA_PD)) {
+							addr.s6_addr32[1] |= htonl(a->assigned);
+
+							if (IN6_ARE_ADDR_EQUAL(&p->addr, &addr) &&
+									p->prefix == a->length)
+								found = true;
+						} else {
+							addr.s6_addr32[3] = htonl(a->assigned);
+
+							if (IN6_ARE_ADDR_EQUAL(&n->addr, &addr))
+								found = true;
+						}
+					}
+				}
+
+				if (!found) {
+					if (otype == DHCPV6_OPT_IA_PREFIX) {
+						struct dhcpv6_ia_prefix inv = {
+							.type = htons(DHCPV6_OPT_IA_PREFIX),
+							.len = htons(sizeof(inv) - 4),
+							.preferred = 0,
+							.valid = 0,
+							.prefix = p->prefix,
+							.addr = p->addr
+						};
+
+						if (datalen + sizeof(inv) > buflen)
+							continue;
+
+						memcpy(buf + datalen, &inv, sizeof(inv));
+						datalen += sizeof(inv);
+					} else {
+						struct dhcpv6_ia_addr inv = {
+							.type = htons(DHCPV6_OPT_IA_ADDR),
+							.len = htons(sizeof(inv) - 4),
+							.addr = n->addr,
+							.preferred = 0,
+							.valid = 0
+						};
+
+						if (datalen + sizeof(inv) > buflen)
+							continue;
+
+						memcpy(buf + datalen, &inv, sizeof(inv));
+						datalen += sizeof(inv);
+					}
+				}
+			}
+		}
+	}
+
+	out.len = htons(datalen - 4);
+	memcpy(buf, &out, sizeof(out));
+	return datalen;
+}
+
+
+size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
+		const struct sockaddr_in6 *addr, const void *data, const uint8_t *end)
+{
+	time_t now = odhcpd_time();
+	size_t response_len = 0;
+	const struct dhcpv6_client_header *hdr = data;
+	uint8_t *start = (uint8_t*)&hdr[1], *odata;
+	uint16_t otype, olen;
+
+	// Find and parse client-id and hostname
+	bool accept_reconf = false;
+	uint8_t *clid_data = NULL, clid_len = 0, mac[6] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
+	char hostname[256];
+	size_t hostname_len = 0;
+	dhcpv6_for_each_option(start, end, otype, olen, odata) {
+		if (otype == DHCPV6_OPT_CLIENTID) {
+			clid_data = odata;
+			clid_len = olen;
+
+			if (olen == 14 && odata[0] == 0 && odata[1] == 1)
+				memcpy(mac, &odata[8], sizeof(mac));
+			else if (olen == 10 && odata[0] == 0 && odata[1] == 3)
+				memcpy(mac, &odata[4], sizeof(mac));
+		} else if (otype == DHCPV6_OPT_FQDN && olen >= 2 && olen <= 255) {
+			uint8_t fqdn_buf[256];
+			memcpy(fqdn_buf, odata, olen);
+			fqdn_buf[olen++] = 0;
+
+			if (dn_expand(&fqdn_buf[1], &fqdn_buf[olen], &fqdn_buf[1], hostname, sizeof(hostname)) > 0)
+				hostname_len = strcspn(hostname, ".");
+		} else if (otype == DHCPV6_OPT_RECONF_ACCEPT) {
+			accept_reconf = true;
+		}
+	}
+
+	if (!clid_data || !clid_len || clid_len > 130)
+		goto out;
+
+	update(iface);
+	bool update_state = false;
+
+	struct dhcpv6_assignment *first = NULL;
+	dhcpv6_for_each_option(start, end, otype, olen, odata) {
+		bool is_pd = (otype == DHCPV6_OPT_IA_PD);
+		bool is_na = (otype == DHCPV6_OPT_IA_NA);
+		if (!is_pd && !is_na)
+			continue;
+
+		struct dhcpv6_ia_hdr *ia = (struct dhcpv6_ia_hdr*)&odata[-4];
+		size_t ia_response_len = 0;
+		uint8_t reqlen = (is_pd) ? 62 : 128;
+		uint32_t reqhint = 0;
+
+		// Parse request hint for IA-PD
+		if (is_pd) {
+			uint8_t *sdata;
+			uint16_t stype, slen;
+			dhcpv6_for_each_option(&ia[1], odata + olen, stype, slen, sdata) {
+				if (stype == DHCPV6_OPT_IA_PREFIX && slen >= sizeof(struct dhcpv6_ia_prefix) - 4) {
+					struct dhcpv6_ia_prefix *p = (struct dhcpv6_ia_prefix*)&sdata[-4];
+					if (p->prefix) {
+						reqlen = p->prefix;
+						reqhint = ntohl(p->addr.s6_addr32[1]);
+						if (reqlen > 32 && reqlen <= 64)
+							reqhint &= (1U << (64 - reqlen)) - 1;
+					}
+					break;
+				}
+			}
+
+			if (reqlen > 64)
+				reqlen = 64;
+		}
+
+		// Find assignment
+		struct dhcpv6_assignment *c, *a = NULL;
+		list_for_each_entry(c, &iface->ia_assignments, head) {
+			if (((c->clid_len == clid_len && !memcmp(c->clid_data, clid_data, clid_len)) ||
+					(c->clid_len >= clid_len && !c->clid_data[0] && !c->clid_data[1]
+					        && !memcmp(c->mac, mac, sizeof(mac)))) &&
+					(c->iaid == ia->iaid || c->valid_until < now) &&
+					((is_pd && c->length <= 64) || (is_na && c->length == 128))) {
+				a = c;
+
+				// Reset state
+				apply_lease(iface, a, false);
+				memcpy(a->clid_data, clid_data, clid_len);
+				a->clid_len = clid_len;
+				a->iaid = ia->iaid;
+				a->peer = *addr;
+				a->reconf_cnt = 0;
+				a->reconf_sent = 0;
+				break;
+			}
+		}
+
+		// Generic message handling
+		uint16_t status = DHCPV6_STATUS_OK;
+		if (hdr->msg_type == DHCPV6_MSG_SOLICIT || hdr->msg_type == DHCPV6_MSG_REQUEST) {
+			bool assigned = !!a;
+
+			if (!a && !iface->no_dynamic_dhcp) { // Create new binding
+				a = calloc(1, sizeof(*a) + clid_len);
+				a->clid_len = clid_len;
+				a->iaid = ia->iaid;
+				a->length = reqlen;
+				a->peer = *addr;
+				a->assigned = reqhint;
+				if (first)
+					memcpy(a->key, first->key, sizeof(a->key));
+				else
+					odhcpd_urandom(a->key, sizeof(a->key));
+				memcpy(a->clid_data, clid_data, clid_len);
+
+				if (is_pd)
+					while (!(assigned = assign_pd(iface, a)) && ++a->length <= 64);
+				else
+					assigned = assign_na(iface, a);
+			}
+
+			if (!assigned || iface->ia_addr_len == 0) { // Set error status
+				status = (is_pd) ? DHCPV6_STATUS_NOPREFIXAVAIL : DHCPV6_STATUS_NOADDRSAVAIL;
+			} else if (assigned && !first) { //
+				size_t handshake_len = 4;
+				buf[0] = 0;
+				buf[1] = DHCPV6_OPT_RECONF_ACCEPT;
+				buf[2] = 0;
+				buf[3] = 0;
+
+				if (hdr->msg_type == DHCPV6_MSG_REQUEST) {
+					struct dhcpv6_auth_reconfigure auth = {
+						htons(DHCPV6_OPT_AUTH),
+						htons(sizeof(auth) - 4),
+						3, 1, 0,
+						{htonl(time(NULL)), htonl(++serial)},
+						1,
+						{0}
+					};
+					memcpy(auth.key, a->key, sizeof(a->key));
+					memcpy(buf + handshake_len, &auth, sizeof(auth));
+					handshake_len += sizeof(auth);
+				}
+
+				buf += handshake_len;
+				buflen -= handshake_len;
+				response_len += handshake_len;
+
+				first = a;
+			}
+
+			ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true);
+
+			// Was only a solicitation: mark binding for removal
+			if (assigned && hdr->msg_type == DHCPV6_MSG_SOLICIT) {
+				a->valid_until = 0;
+			} else if (assigned && hdr->msg_type == DHCPV6_MSG_REQUEST) {
+				if (hostname_len > 0) {
+					a->hostname = realloc(a->hostname, hostname_len + 1);
+					memcpy(a->hostname, hostname, hostname_len);
+					a->hostname[hostname_len] = 0;
+				}
+				a->accept_reconf = accept_reconf;
+				apply_lease(iface, a, true);
+				update_state = true;
+			} else if (!assigned && a) { // Cleanup failed assignment
+				free(a->hostname);
+				free(a);
+			}
+		} else if (hdr->msg_type == DHCPV6_MSG_RENEW ||
+				hdr->msg_type == DHCPV6_MSG_RELEASE ||
+				hdr->msg_type == DHCPV6_MSG_REBIND ||
+				hdr->msg_type == DHCPV6_MSG_DECLINE) {
+			if (!a && hdr->msg_type != DHCPV6_MSG_REBIND) {
+				status = DHCPV6_STATUS_NOBINDING;
+				ia_response_len = append_reply(buf, buflen, status, ia, a, iface, false);
+			} else if (hdr->msg_type == DHCPV6_MSG_RENEW ||
+					hdr->msg_type == DHCPV6_MSG_REBIND) {
+				ia_response_len = append_reply(buf, buflen, status, ia, a, iface, false);
+				if (a)
+					apply_lease(iface, a, true);
+			} else if (hdr->msg_type == DHCPV6_MSG_RELEASE) {
+				a->valid_until = 0;
+				apply_lease(iface, a, false);
+				update_state = true;
+			} else if (hdr->msg_type == DHCPV6_MSG_DECLINE && a->length == 128) {
+				a->clid_len = 0;
+				a->valid_until = now + 3600; // Block address for 1h
+				update_state = true;
+			}
+		} else if (hdr->msg_type == DHCPV6_MSG_CONFIRM) {
+			// Always send NOTONLINK for CONFIRM so that clients restart connection
+			status = DHCPV6_STATUS_NOTONLINK;
+			ia_response_len = append_reply(buf, buflen, status, ia, a, iface, true);
+		}
+
+		buf += ia_response_len;
+		buflen -= ia_response_len;
+		response_len += ia_response_len;
+	}
+
+	if (hdr->msg_type == DHCPV6_MSG_RELEASE && response_len + 6 < buflen) {
+		buf[0] = 0;
+		buf[1] = DHCPV6_OPT_STATUS;
+		buf[2] = 0;
+		buf[3] = 2;
+		buf[4] = 0;
+		buf[5] = DHCPV6_STATUS_OK;
+		response_len += 6;
+	}
+
+	if (update_state)
+		dhcpv6_write_statefile();
+
+out:
+	return response_len;
+}

+ 452 - 0
src/dhcpv6.c

@@ -0,0 +1,452 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ *
+ */
+
+#include <errno.h>
+#include <unistd.h>
+#include <stddef.h>
+#include <resolv.h>
+#include <sys/timerfd.h>
+
+#include "odhcpd.h"
+#include "dhcpv6.h"
+
+
+static void relay_client_request(struct sockaddr_in6 *source,
+		const void *data, size_t len, struct interface *iface);
+static void relay_server_response(uint8_t *data, size_t len);
+
+static void handle_dhcpv6(void *addr, void *data, size_t len,
+		struct interface *iface);
+static void handle_client_request(void *addr, void *data, size_t len,
+		struct interface *iface);
+
+static struct odhcpd_event dhcpv6_event = {{.fd = -1}, handle_dhcpv6};
+
+
+
+// Create socket and register events
+int init_dhcpv6(void)
+{
+	int sock = socket(AF_INET6, SOCK_DGRAM | SOCK_CLOEXEC, IPPROTO_UDP);
+
+	// Basic IPv6 configuration
+	int val = 1;
+	setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &val, sizeof(val));
+	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
+	setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, sizeof(val));
+
+	val = DHCPV6_HOP_COUNT_LIMIT;
+	setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val));
+
+	val = 0;
+	setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val));
+
+	struct sockaddr_in6 bind_addr = {AF_INET6, htons(DHCPV6_SERVER_PORT),
+				0, IN6ADDR_ANY_INIT, 0};
+
+	if (bind(sock, (struct sockaddr*)&bind_addr, sizeof(bind_addr))) {
+		syslog(LOG_ERR, "Failed to open DHCPv6 server socket: %s",
+				strerror(errno));
+		return -1;
+	}
+
+	dhcpv6_event.uloop.fd = sock;
+	odhcpd_register(&dhcpv6_event);
+
+	dhcpv6_ia_init(dhcpv6_event.uloop.fd);
+
+	return 0;
+}
+
+
+int setup_dhcpv6_interface(struct interface *iface, bool enable)
+{
+	// Configure multicast settings
+	struct ipv6_mreq relay = {ALL_DHCPV6_RELAYS, iface->ifindex};
+	struct ipv6_mreq server = {ALL_DHCPV6_SERVERS, iface->ifindex};
+
+	setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6,
+			IPV6_DROP_MEMBERSHIP, &relay, sizeof(relay));
+	setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6,
+			IPV6_DROP_MEMBERSHIP, &server, sizeof(server));
+
+	if (enable && iface->dhcpv6 && !iface->master) {
+		setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6,
+				IPV6_ADD_MEMBERSHIP, &relay, sizeof(relay));
+
+		if (iface->dhcpv6 == RELAYD_SERVER)
+			setsockopt(dhcpv6_event.uloop.fd, IPPROTO_IPV6,
+					IPV6_ADD_MEMBERSHIP, &server, sizeof(server));
+	}
+
+	setup_dhcpv6_ia_interface(iface, enable);
+	return 0;
+}
+
+
+static void handle_nested_message(uint8_t *data, size_t len,
+		uint8_t **opts, uint8_t **end, struct iovec iov[6])
+{
+	struct dhcpv6_relay_header *hdr = (struct dhcpv6_relay_header*)data;
+	if (iov[0].iov_base == NULL) {
+		iov[0].iov_base = data;
+		iov[0].iov_len = len;
+	}
+
+	if (len < sizeof(struct dhcpv6_client_header))
+		return;
+
+	if (hdr->msg_type != DHCPV6_MSG_RELAY_FORW) {
+		iov[0].iov_len = data - (uint8_t*)iov[0].iov_base;
+		struct dhcpv6_client_header *hdr = (void*)data;
+		*opts = (uint8_t*)&hdr[1];
+		*end = data + len;
+		return;
+	}
+
+	uint16_t otype, olen;
+	uint8_t *odata;
+	dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) {
+		if (otype == DHCPV6_OPT_RELAY_MSG) {
+			iov[7].iov_base = odata + olen;
+			iov[7].iov_len = (((uint8_t*)iov[0].iov_base) + iov[0].iov_len)
+					- (odata + olen);
+			handle_nested_message(odata, olen, opts, end, iov);
+			return;
+		}
+	}
+}
+
+
+static void update_nested_message(uint8_t *data, size_t len, ssize_t pdiff)
+{
+	struct dhcpv6_relay_header *hdr = (struct dhcpv6_relay_header*)data;
+	if (hdr->msg_type != DHCPV6_MSG_RELAY_FORW)
+		return;
+
+	hdr->msg_type = DHCPV6_MSG_RELAY_REPL;
+
+	uint16_t otype, olen;
+	uint8_t *odata;
+	dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) {
+		if (otype == DHCPV6_OPT_RELAY_MSG) {
+			olen += pdiff;
+			odata[-2] = (olen >> 8) & 0xff;
+			odata[-1] = olen & 0xff;
+			update_nested_message(odata, olen - pdiff, pdiff);
+			return;
+		}
+	}
+}
+
+
+// Simple DHCPv6-server for information requests
+static void handle_client_request(void *addr, void *data, size_t len,
+		struct interface *iface)
+{
+	struct dhcpv6_client_header *hdr = data;
+	if (len < sizeof(*hdr))
+		return;
+
+	syslog(LOG_NOTICE, "Got DHCPv6 request");
+
+	// Construct reply message
+	struct __attribute__((packed)) {
+		uint8_t msg_type;
+		uint8_t tr_id[3];
+		uint16_t serverid_type;
+		uint16_t serverid_length;
+		uint16_t duid_type;
+		uint16_t hardware_type;
+		uint8_t mac[6];
+		uint16_t clientid_type;
+		uint16_t clientid_length;
+		uint8_t clientid_buf[130];
+	} dest = {
+		.msg_type = DHCPV6_MSG_REPLY,
+		.serverid_type = htons(DHCPV6_OPT_SERVERID),
+		.serverid_length = htons(10),
+		.duid_type = htons(3),
+		.hardware_type = htons(1),
+		.clientid_type = htons(DHCPV6_OPT_CLIENTID),
+		.clientid_buf = {0}
+	};
+	odhcpd_get_mac(iface, dest.mac);
+
+	struct __attribute__((packed)) {
+		uint16_t type;
+		uint16_t len;
+		uint16_t value;
+	} stat = {htons(DHCPV6_OPT_STATUS), htons(sizeof(stat) - 4),
+			htons(DHCPV6_STATUS_NOADDRSAVAIL)};
+
+	struct __attribute__((packed)) {
+		uint16_t type;
+		uint16_t len;
+		uint32_t value;
+	} refresh = {htons(DHCPV6_OPT_INFO_REFRESH), htons(sizeof(uint32_t)),
+			htonl(600)};
+
+	struct odhcpd_ipaddr ipaddr;
+	struct in6_addr *dns_addr = iface->dns;
+	size_t dns_cnt = iface->dns_cnt;
+
+	if (dns_cnt == 0 && odhcpd_get_interface_addresses(iface->ifindex, &ipaddr, 1) == 1) {
+		dns_addr = &ipaddr.addr;
+		dns_cnt = 1;
+	}
+
+	struct {
+		uint16_t type;
+		uint16_t len;
+	} dns = {htons(DHCPV6_OPT_DNS_SERVERS), htons(dns_cnt * sizeof(*dns_addr))};
+
+
+
+	// DNS Search options
+	uint8_t search_buf[256], *search_domain = iface->search;
+	size_t search_len = iface->search_len;
+
+	if (!search_domain && !res_init() && _res.dnsrch[0] && _res.dnsrch[0][0]) {
+		int len = dn_comp(_res.dnsrch[0], search_buf,
+				sizeof(search_buf), NULL, NULL);
+		if (len > 0) {
+			search_domain = search_buf;
+			search_len = len;
+		}
+	}
+
+	struct {
+		uint16_t type;
+		uint16_t len;
+	} search = {htons(DHCPV6_OPT_DNS_DOMAIN), htons(search_len)};
+
+
+
+	uint8_t pdbuf[512];
+	struct iovec iov[] = {{NULL, 0},
+			{&dest, (uint8_t*)&dest.clientid_type - (uint8_t*)&dest},
+			{&dns, (dns_cnt) ? sizeof(dns) : 0},
+			{dns_addr, dns_cnt * sizeof(*dns_addr)},
+			{&search, (search_len) ? sizeof(search) : 0},
+			{search_domain, search_len},
+			{pdbuf, 0},
+			{NULL, 0}};
+
+	uint8_t *opts = (uint8_t*)&hdr[1], *opts_end = (uint8_t*)data + len;
+	if (hdr->msg_type == DHCPV6_MSG_RELAY_FORW)
+		handle_nested_message(data, len, &opts, &opts_end, iov);
+
+	memcpy(dest.tr_id, &opts[-3], sizeof(dest.tr_id));
+
+	if (opts[-4] == DHCPV6_MSG_ADVERTISE || opts[-4] == DHCPV6_MSG_REPLY || opts[-4] == DHCPV6_MSG_RELAY_REPL)
+		return;
+
+	if (opts[-4] == DHCPV6_MSG_SOLICIT) {
+		dest.msg_type = DHCPV6_MSG_ADVERTISE;
+	} else if (opts[-4] == DHCPV6_MSG_INFORMATION_REQUEST) {
+		iov[6].iov_base = &refresh;
+		iov[6].iov_len = sizeof(refresh);
+	}
+
+	// Go through options and find what we need
+	uint16_t otype, olen;
+	uint8_t *odata;
+	dhcpv6_for_each_option(opts, opts_end, otype, olen, odata) {
+		if (otype == DHCPV6_OPT_CLIENTID && olen <= 130) {
+			dest.clientid_length = htons(olen);
+			memcpy(dest.clientid_buf, odata, olen);
+			iov[1].iov_len += 4 + olen;
+		} else if (otype == DHCPV6_OPT_SERVERID) {
+			if (olen != ntohs(dest.serverid_length) ||
+					memcmp(odata, &dest.duid_type, olen))
+				return; // Not for us
+		}
+	}
+
+	if (opts[-4] != DHCPV6_MSG_INFORMATION_REQUEST) {
+		iov[6].iov_len = dhcpv6_handle_ia(pdbuf, sizeof(pdbuf), iface, addr, &opts[-4], opts_end);
+		if (iov[6].iov_len == 0 && opts[-4] == DHCPV6_MSG_REBIND)
+			return;
+	}
+
+	if (iov[0].iov_len > 0) // Update length
+		update_nested_message(data, len, iov[1].iov_len + iov[2].iov_len +
+				iov[3].iov_len + iov[4].iov_len + iov[5].iov_len +
+				iov[6].iov_len - (4 + opts_end - opts));
+
+	odhcpd_send(dhcpv6_event.uloop.fd, addr, iov, ARRAY_SIZE(iov), iface);
+}
+
+
+// Central DHCPv6-relay handler
+static void handle_dhcpv6(void *addr, void *data, size_t len,
+		struct interface *iface)
+{
+	if (iface->dhcpv6 == RELAYD_SERVER) {
+		handle_client_request(addr, data, len, iface);
+	} else if (iface->dhcpv6 == RELAYD_RELAY) {
+		if (iface->master)
+			relay_server_response(data, len);
+		else
+			relay_client_request(addr, data, len, iface);
+	}
+}
+
+
+// Relay server response (regular relay server handling)
+static void relay_server_response(uint8_t *data, size_t len)
+{
+	// Information we need to gather
+	uint8_t *payload_data = NULL;
+	size_t payload_len = 0;
+	int32_t ifaceidx = 0;
+	struct sockaddr_in6 target = {AF_INET6, htons(DHCPV6_CLIENT_PORT),
+		0, IN6ADDR_ANY_INIT, 0};
+
+	syslog(LOG_NOTICE, "Got a DHCPv6-reply");
+
+	int otype, olen;
+	uint8_t *odata, *end = data + len;
+
+	// Relay DHCPv6 reply from server to client
+	struct dhcpv6_relay_header *h = (void*)data;
+	if (len < sizeof(*h) || h->msg_type != DHCPV6_MSG_RELAY_REPL)
+		return;
+
+	memcpy(&target.sin6_addr, &h->peer_address,
+			sizeof(struct in6_addr));
+
+	// Go through options and find what we need
+	dhcpv6_for_each_option(h->options, end, otype, olen, odata) {
+		if (otype == DHCPV6_OPT_INTERFACE_ID
+				&& olen == sizeof(ifaceidx)) {
+			memcpy(&ifaceidx, odata, sizeof(ifaceidx));
+		} else if (otype == DHCPV6_OPT_RELAY_MSG) {
+			payload_data = odata;
+			payload_len = olen;
+		}
+	}
+
+	// Invalid interface-id or basic payload
+	struct interface *iface = odhcpd_get_interface_by_index(ifaceidx);
+	if (!iface || iface->master || !payload_data || payload_len < 4)
+		return;
+
+	bool is_authenticated = false;
+	struct in6_addr *dns_ptr = NULL;
+	size_t dns_count = 0;
+
+	// If the payload is relay-reply we have to send to the server port
+	if (payload_data[0] == DHCPV6_MSG_RELAY_REPL) {
+		target.sin6_port = htons(DHCPV6_SERVER_PORT);
+	} else { // Go through the payload data
+		struct dhcpv6_client_header *h = (void*)payload_data;
+		end = payload_data + payload_len;
+
+		dhcpv6_for_each_option(&h[1], end, otype, olen, odata) {
+			if (otype == DHCPV6_OPT_DNS_SERVERS && olen >= 16) {
+				dns_ptr = (struct in6_addr*)odata;
+				dns_count = olen / 16;
+			} else if (otype == DHCPV6_OPT_AUTH) {
+				is_authenticated = true;
+			}
+		}
+	}
+
+	// Rewrite DNS servers if requested
+	if (iface->always_rewrite_dns && dns_ptr && dns_count > 0) {
+		if (is_authenticated)
+			return; // Impossible to rewrite
+
+		struct odhcpd_ipaddr ip;
+		const struct in6_addr *rewrite = iface->dns;
+		size_t rewrite_cnt = iface->dns_cnt;
+
+		if (rewrite_cnt == 0) {
+			if (odhcpd_get_interface_addresses(iface->ifindex, &ip, 1) < 1)
+				return; // Unable to get interface address
+
+			rewrite = &ip.addr;
+			rewrite_cnt = 1;
+		}
+
+		// Copy over any other addresses
+		for (size_t i = 0; i < dns_count; ++i) {
+			size_t j = (i < rewrite_cnt) ? i : rewrite_cnt - 1;
+			memcpy(&dns_ptr[i], &rewrite[j], sizeof(*rewrite));
+		}
+	}
+
+	struct iovec iov = {payload_data, payload_len};
+	odhcpd_send(dhcpv6_event.uloop.fd, &target, &iov, 1, iface);
+}
+
+
+// Relay client request (regular DHCPv6-relay)
+static void relay_client_request(struct sockaddr_in6 *source,
+		const void *data, size_t len, struct interface *iface)
+{
+	struct interface *master = odhcpd_get_master_interface();
+	const struct dhcpv6_relay_header *h = data;
+	if (!master || master->dhcpv6 != RELAYD_RELAY ||
+			h->msg_type == DHCPV6_MSG_RELAY_REPL ||
+			h->msg_type == DHCPV6_MSG_RECONFIGURE ||
+			h->msg_type == DHCPV6_MSG_REPLY ||
+			h->msg_type == DHCPV6_MSG_ADVERTISE)
+		return; // Invalid message types for client
+
+	syslog(LOG_NOTICE, "Got a DHCPv6-request");
+
+	// Construct our forwarding envelope
+	struct dhcpv6_relay_forward_envelope hdr = {
+		.msg_type = DHCPV6_MSG_RELAY_FORW,
+		.hop_count = 0,
+		.interface_id_type = htons(DHCPV6_OPT_INTERFACE_ID),
+		.interface_id_len = htons(sizeof(uint32_t)),
+		.relay_message_type = htons(DHCPV6_OPT_RELAY_MSG),
+		.relay_message_len = htons(len),
+	};
+
+	if (h->msg_type == DHCPV6_MSG_RELAY_FORW) { // handle relay-forward
+		if (h->hop_count >= DHCPV6_HOP_COUNT_LIMIT)
+			return; // Invalid hop count
+		else
+			hdr.hop_count = h->hop_count + 1;
+	}
+
+	// use memcpy here as the destination fields are unaligned
+	uint32_t ifindex = iface->ifindex;
+	memcpy(&hdr.peer_address, &source->sin6_addr, sizeof(struct in6_addr));
+	memcpy(&hdr.interface_id_data, &ifindex, sizeof(ifindex));
+
+	// Detect public IP of slave interface to use as link-address
+	struct odhcpd_ipaddr ip;
+	if (odhcpd_get_interface_addresses(iface->ifindex, &ip, 1) < 1) {
+		// No suitable address! Is the slave not configured yet?
+		// Detect public IP of master interface and use it instead
+		// This is WRONG and probably violates the RFC. However
+		// otherwise we have a hen and egg problem because the
+		// slave-interface cannot be auto-configured.
+		if (odhcpd_get_interface_addresses(master->ifindex, &ip, 1) < 1)
+			return; // Could not obtain a suitable address
+	}
+	memcpy(&hdr.link_address, &ip.addr, sizeof(hdr.link_address));
+
+	struct sockaddr_in6 dhcpv6_servers = {AF_INET6,
+			htons(DHCPV6_SERVER_PORT), 0, ALL_DHCPV6_SERVERS, 0};
+	struct iovec iov[2] = {{&hdr, sizeof(hdr)}, {(void*)data, len}};
+	odhcpd_send(dhcpv6_event.uloop.fd, &dhcpv6_servers, iov, 2, master);
+}

+ 162 - 0
src/dhcpv6.h

@@ -0,0 +1,162 @@
+/**
+ *   Copyright (C) 2012 Steven Barth <steven@midlink.org>
+ *
+ *   This program is free software; you can redistribute it and/or modify
+ *   it under the terms of the GNU General Public License version 2
+ *   as published by the Free Software Foundation.
+ *
+ *   This program is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *   GNU General Public License version 2 for more details.
+ *
+ */
+#pragma once
+
+#define ALL_DHCPV6_RELAYS {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02}}}
+
+#define ALL_DHCPV6_SERVERS {{{0xff, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03}}}
+
+#define DHCPV6_CLIENT_PORT 546
+#define DHCPV6_SERVER_PORT 547
+
+#define DHCPV6_MSG_SOLICIT 1
+#define DHCPV6_MSG_ADVERTISE 2
+#define DHCPV6_MSG_REQUEST 3
+#define DHCPV6_MSG_CONFIRM 4
+#define DHCPV6_MSG_RENEW 5
+#define DHCPV6_MSG_REBIND 6
+#define DHCPV6_MSG_REPLY 7
+#define DHCPV6_MSG_RELEASE 8
+#define DHCPV6_MSG_DECLINE 9
+#define DHCPV6_MSG_RECONFIGURE 10
+#define DHCPV6_MSG_INFORMATION_REQUEST 11
+#define DHCPV6_MSG_RELAY_FORW 12
+#define DHCPV6_MSG_RELAY_REPL 13
+
+#define DHCPV6_OPT_CLIENTID 1
+#define DHCPV6_OPT_SERVERID 2
+#define DHCPV6_OPT_IA_NA 3
+#define DHCPV6_OPT_IA_ADDR 5
+#define DHCPV6_OPT_STATUS 13
+#define DHCPV6_OPT_RELAY_MSG 9
+#define DHCPV6_OPT_AUTH 11
+#define DHCPV6_OPT_INTERFACE_ID 18
+#define DHCPV6_OPT_RECONF_MSG 19
+#define DHCPV6_OPT_RECONF_ACCEPT 20
+#define DHCPV6_OPT_DNS_SERVERS 23
+#define DHCPV6_OPT_DNS_DOMAIN 24
+#define DHCPV6_OPT_IA_PD 25
+#define DHCPV6_OPT_IA_PREFIX 26
+#define DHCPV6_OPT_INFO_REFRESH 32
+#define DHCPV6_OPT_FQDN 39
+
+#define DHCPV6_DUID_VENDOR 2
+
+#define DHCPV6_STATUS_OK 0
+#define DHCPV6_STATUS_NOADDRSAVAIL 2
+#define DHCPV6_STATUS_NOBINDING 3
+#define DHCPV6_STATUS_NOTONLINK 4
+#define DHCPV6_STATUS_NOPREFIXAVAIL 6
+
+// I just remembered I have an old one lying around...
+#define DHCPV6_ENT_NO  30462
+#define DHCPV6_ENT_TYPE 1
+
+
+#define DHCPV6_HOP_COUNT_LIMIT 32
+
+struct dhcpv6_client_header {
+	uint8_t msg_type;
+	uint8_t transaction_id[3];
+} __attribute__((packed));
+
+struct dhcpv6_relay_header {
+	uint8_t msg_type;
+	uint8_t hop_count;
+	struct in6_addr link_address;
+	struct in6_addr peer_address;
+	uint8_t options[];
+} __attribute__((packed));
+
+struct dhcpv6_relay_forward_envelope {
+	uint8_t msg_type;
+	uint8_t hop_count;
+	struct in6_addr link_address;
+	struct in6_addr peer_address;
+	uint16_t interface_id_type;
+	uint16_t interface_id_len;
+	uint32_t interface_id_data;
+	uint16_t relay_message_type;
+	uint16_t relay_message_len;
+} __attribute__((packed));
+
+struct dhcpv6_auth_reconfigure {
+	uint16_t type;
+	uint16_t len;
+	uint8_t protocol;
+	uint8_t algorithm;
+	uint8_t rdm;
+	uint32_t replay[2];
+	uint8_t reconf_type;
+	uint8_t key[16];
+} _packed;
+
+struct dhcpv6_ia_hdr {
+	uint16_t type;
+	uint16_t len;
+	uint32_t iaid;
+	uint32_t t1;
+	uint32_t t2;
+} _packed;
+
+struct dhcpv6_ia_prefix {
+	uint16_t type;
+	uint16_t len;
+	uint32_t preferred;
+	uint32_t valid;
+	uint8_t prefix;
+	struct in6_addr addr;
+} _packed;
+
+struct dhcpv6_ia_addr {
+	uint16_t type;
+	uint16_t len;
+	struct in6_addr addr;
+	uint32_t preferred;
+	uint32_t valid;
+} _packed;
+
+struct dhcpv6_assignment {
+	struct list_head head;
+	struct sockaddr_in6 peer;
+	time_t valid_until;
+	time_t reconf_sent;
+	int reconf_cnt;
+	char *hostname;
+	uint8_t key[16];
+	uint32_t assigned;
+	uint32_t iaid;
+	uint8_t mac[6];
+	uint8_t length; // length == 128 -> IA_NA, length <= 64 -> IA_PD
+	bool accept_reconf;
+	uint8_t clid_len;
+	uint8_t clid_data[];
+};
+
+
+
+#define dhcpv6_for_each_option(start, end, otype, olen, odata)\
+	for (uint8_t *_o = (uint8_t*)(start); _o + 4 <= (end) &&\
+		((otype) = _o[0] << 8 | _o[1]) && ((odata) = (void*)&_o[4]) &&\
+		((olen) = _o[2] << 8 | _o[3]) + (odata) <= (end); \
+		_o += 4 + (_o[2] << 8 | _o[3]))
+
+int dhcpv6_init_ia(struct interface *iface, int socket);
+size_t dhcpv6_handle_ia(uint8_t *buf, size_t buflen, struct interface *iface,
+		const struct sockaddr_in6 *addr, const void *data, const uint8_t *end);
+int dhcpv6_ia_init(int dhcpv6_socket);
+int setup_dhcpv6_ia_interface(struct interface *iface, bool enable);
+void dhcpv6_write_statefile(void);

+ 242 - 0
src/md5.c

@@ -0,0 +1,242 @@
+/*
+ *  md5.c - Compute MD5 checksum of strings according to the
+ *          definition of MD5 in RFC 1321 from April 1992.
+ *
+ *  Written by Ulrich Drepper <drepper@gnu.ai.mit.edu>, 1995.
+ *
+ *  Copyright (C) 1995-1999 Free Software Foundation, Inc.
+ *  Copyright (C) 2001 Manuel Novoa III
+ *  Copyright (C) 2003 Glenn L. McGrath
+ *  Copyright (C) 2003 Erik Andersen
+ *
+ *  Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
+ */
+
+#include <libubox/blob.h> /* TODO: better include for bswap_32 compat */
+#include "md5.h"
+
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+#define SWAP_LE32(x) (x)
+#else
+#define SWAP_LE32(x) bswap_32(x)
+#endif
+
+/* Initialize structure containing state of computation.
+ * (RFC 1321, 3.3: Step 3)
+ */
+void md5_begin(md5_ctx_t *ctx)
+{
+	ctx->A = 0x67452301;
+	ctx->B = 0xefcdab89;
+	ctx->C = 0x98badcfe;
+	ctx->D = 0x10325476;
+
+	ctx->total = 0;
+	ctx->buflen = 0;
+}
+
+/* These are the four functions used in the four steps of the MD5 algorithm
+ * and defined in the RFC 1321.  The first function is a little bit optimized
+ * (as found in Colin Plumbs public domain implementation).
+ * #define FF(b, c, d) ((b & c) | (~b & d))
+ */
+# define FF(b, c, d) (d ^ (b & (c ^ d)))
+# define FG(b, c, d) FF (d, b, c)
+# define FH(b, c, d) (b ^ c ^ d)
+# define FI(b, c, d) (c ^ (b | ~d))
+
+/* Hash a single block, 64 bytes long and 4-byte aligned. */
+static void md5_hash_block(const void *buffer, md5_ctx_t *ctx)
+{
+	uint32_t correct_words[16];
+	const uint32_t *words = buffer;
+
+	static const uint32_t C_array[] = {
+		/* round 1 */
+		0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
+		0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
+		0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
+		0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
+		/* round 2 */
+		0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
+		0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8,
+		0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
+		0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
+		/* round 3 */
+		0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
+		0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
+		0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05,
+		0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
+		/* round 4 */
+		0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
+		0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
+		0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
+		0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
+	};
+
+	static const char P_array[] = {
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,	/* 1 */
+		1, 6, 11, 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12,	/* 2 */
+		5, 8, 11, 14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, 15, 2,	/* 3 */
+		0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9	/* 4 */
+	};
+
+	static const char S_array[] = {
+		7, 12, 17, 22,
+		5, 9, 14, 20,
+		4, 11, 16, 23,
+		6, 10, 15, 21
+	};
+
+	uint32_t A = ctx->A;
+	uint32_t B = ctx->B;
+	uint32_t C = ctx->C;
+	uint32_t D = ctx->D;
+
+	uint32_t *cwp = correct_words;
+
+#  define CYCLIC(w, s) (w = (w << s) | (w >> (32 - s)))
+
+	const uint32_t *pc;
+	const char *pp;
+	const char *ps;
+	int i;
+	uint32_t temp;
+
+	for (i = 0; i < 16; i++) {
+		cwp[i] = SWAP_LE32(words[i]);
+	}
+	words += 16;
+
+	pc = C_array;
+	pp = P_array;
+	ps = S_array;
+
+	for (i = 0; i < 16; i++) {
+		temp = A + FF(B, C, D) + cwp[(int) (*pp++)] + *pc++;
+		CYCLIC(temp, ps[i & 3]);
+		temp += B;
+		A = D;
+		D = C;
+		C = B;
+		B = temp;
+	}
+
+	ps += 4;
+	for (i = 0; i < 16; i++) {
+		temp = A + FG(B, C, D) + cwp[(int) (*pp++)] + *pc++;
+		CYCLIC(temp, ps[i & 3]);
+		temp += B;
+		A = D;
+		D = C;
+		C = B;
+		B = temp;
+	}
+	ps += 4;
+	for (i = 0; i < 16; i++) {
+		temp = A + FH(B, C, D) + cwp[(int) (*pp++)] + *pc++;
+		CYCLIC(temp, ps[i & 3]);
+		temp += B;
+		A = D;
+		D = C;
+		C = B;
+		B = temp;
+	}
+	ps += 4;
+	for (i = 0; i < 16; i++) {
+		temp = A + FI(B, C, D) + cwp[(int) (*pp++)] + *pc++;
+		CYCLIC(temp, ps[i & 3]);
+		temp += B;
+		A = D;
+		D = C;
+		C = B;
+		B = temp;
+	}
+
+
+	ctx->A += A;
+	ctx->B += B;
+	ctx->C += C;
+	ctx->D += D;
+}
+
+/* Feed data through a temporary buffer to call md5_hash_aligned_block()
+ * with chunks of data that are 4-byte aligned and a multiple of 64 bytes.
+ * This function's internal buffer remembers previous data until it has 64
+ * bytes worth to pass on.  Call md5_end() to flush this buffer. */
+
+void md5_hash(const void *buffer, size_t len, md5_ctx_t *ctx)
+{
+	char *buf = (char *)buffer;
+
+	/* RFC 1321 specifies the possible length of the file up to 2^64 bits,
+	 * Here we only track the number of bytes.  */
+
+	ctx->total += len;
+
+	// Process all input.
+
+	while (len) {
+		unsigned i = 64 - ctx->buflen;
+
+		// Copy data into aligned buffer.
+
+		if (i > len)
+			i = len;
+		memcpy(ctx->buffer + ctx->buflen, buf, i);
+		len -= i;
+		ctx->buflen += i;
+		buf += i;
+
+		// When buffer fills up, process it.
+
+		if (ctx->buflen == 64) {
+			md5_hash_block(ctx->buffer, ctx);
+			ctx->buflen = 0;
+		}
+	}
+}
+
+/* Process the remaining bytes in the buffer and put result from CTX
+ * in first 16 bytes following RESBUF.  The result is always in little
+ * endian byte order, so that a byte-wise output yields to the wanted
+ * ASCII representation of the message digest.
+ *
+ * IMPORTANT: On some systems it is required that RESBUF is correctly
+ * aligned for a 32 bits value.
+ */
+void md5_end(void *resbuf, md5_ctx_t *ctx)
+{
+	char *buf = ctx->buffer;
+	int i;
+
+	/* Pad data to block size.  */
+
+	buf[ctx->buflen++] = 0x80;
+	memset(buf + ctx->buflen, 0, 128 - ctx->buflen);
+
+	/* Put the 64-bit file length in *bits* at the end of the buffer.  */
+	ctx->total <<= 3;
+	if (ctx->buflen > 56)
+		buf += 64;
+
+	for (i = 0; i < 8; i++)
+		buf[56 + i] = ctx->total >> (i*8);
+
+	/* Process last bytes.  */
+	if (buf != ctx->buffer)
+		md5_hash_block(ctx->buffer, ctx);
+	md5_hash_block(buf, ctx);
+
+	/* Put result from CTX in first 16 bytes following RESBUF.  The result is
+	 * always in little endian byte order, so that a byte-wise output yields
+	 * to the wanted ASCII representation of the message digest.
+	 *
+	 * IMPORTANT: On some systems it is required that RESBUF is correctly
+	 * aligned for a 32 bits value.
+	 */
+	((uint32_t *) resbuf)[0] = SWAP_LE32(ctx->A);
+	((uint32_t *) resbuf)[1] = SWAP_LE32(ctx->B);
+	((uint32_t *) resbuf)[2] = SWAP_LE32(ctx->C);
+	((uint32_t *) resbuf)[3] = SWAP_LE32(ctx->D);
+}

+ 17 - 0
src/md5.h

@@ -0,0 +1,17 @@
+#pragma once
+#include <stdint.h>
+#include <stddef.h>
+
+typedef struct md5_ctx {
+	uint32_t A;
+	uint32_t B;
+	uint32_t C;
+	uint32_t D;
+	uint64_t total;
+	uint32_t buflen;
+	char buffer[128];
+} md5_ctx_t;
+
+void md5_begin(md5_ctx_t *ctx);
+void md5_hash(const void *data, size_t length, md5_ctx_t *ctx);
+void md5_end(void *resbuf, md5_ctx_t *ctx);

+ 532 - 0
src/ndp.c

@@ -0,0 +1,532 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <errno.h>
+
+#include <arpa/inet.h>
+#include <sys/socket.h>
+#include <net/ethernet.h>
+#include <netinet/ip6.h>
+#include <netinet/icmp6.h>
+#include <netpacket/packet.h>
+
+#include <linux/rtnetlink.h>
+#include <linux/filter.h>
+#include "router.h"
+#include "ndp.h"
+
+
+
+static void handle_solicit(void *addr, void *data, size_t len,
+		struct interface *iface);
+static void handle_rtnetlink(void *addr, void *data, size_t len,
+		struct interface *iface);
+static struct ndp_neighbor* find_neighbor(struct in6_addr *addr, bool strict);
+static void modify_neighbor(struct in6_addr *addr, struct interface *iface,
+		bool add);
+static ssize_t ping6(struct in6_addr *addr,
+		const struct interface *iface);
+
+static struct list_head neighbors = LIST_HEAD_INIT(neighbors);
+static size_t neighbor_count = 0;
+static uint32_t rtnl_seqid = 0;
+
+static int ping_socket = -1;
+static struct odhcpd_event ndp_event = {{.fd = -1}, handle_solicit};
+static struct odhcpd_event rtnl_event = {{.fd = -1}, handle_rtnetlink};
+
+
+// Filter ICMPv6 messages of type neighbor soliciation
+static struct sock_filter bpf[] = {
+	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, offsetof(struct ip6_hdr, ip6_nxt)),
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3),
+	BPF_STMT(BPF_LD | BPF_B | BPF_ABS, sizeof(struct ip6_hdr) +
+			offsetof(struct icmp6_hdr, icmp6_type)),
+	BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_NEIGHBOR_SOLICIT, 0, 1),
+	BPF_STMT(BPF_RET | BPF_K, 0xffffffff),
+	BPF_STMT(BPF_RET | BPF_K, 0),
+};
+static const struct sock_fprog bpf_prog = {sizeof(bpf) / sizeof(*bpf), bpf};
+
+
+// Initialize NDP-proxy
+int init_ndp(void)
+{
+	// Setup netlink socket
+	if ((rtnl_event.uloop.fd = odhcpd_open_rtnl()) < 0)
+		return -1;
+
+	// Receive netlink neighbor and ip-address events
+	uint32_t group = RTNLGRP_IPV6_IFADDR;
+	setsockopt(rtnl_event.uloop.fd, SOL_NETLINK,
+			NETLINK_ADD_MEMBERSHIP, &group, sizeof(group));
+	group = RTNLGRP_IPV6_ROUTE;
+	setsockopt(rtnl_event.uloop.fd, SOL_NETLINK,
+			NETLINK_ADD_MEMBERSHIP, &group, sizeof(group));
+
+	// Synthesize initial address events
+	struct {
+		struct nlmsghdr nh;
+		struct ifaddrmsg ifa;
+	} req2 = {
+		{sizeof(req2), RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP,
+				++rtnl_seqid, 0},
+		{.ifa_family = AF_INET6}
+	};
+	send(rtnl_event.uloop.fd, &req2, sizeof(req2), MSG_DONTWAIT);
+	odhcpd_register(&rtnl_event);
+
+
+	// Create socket for intercepting NDP
+	int sock = socket(AF_PACKET, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
+			htons(ETH_P_ALL)); // ETH_P_ALL for ingress + egress
+	if (sock < 0) {
+		syslog(LOG_ERR, "Unable to open packet socket: %s",
+				strerror(errno));
+		return -1;
+	}
+
+	if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_FILTER,
+			&bpf_prog, sizeof(bpf_prog))) {
+		syslog(LOG_ERR, "Failed to set BPF: %s", strerror(errno));
+		return -1;
+	}
+
+	ndp_event.uloop.fd = sock;
+	odhcpd_register(&ndp_event);
+
+	// Open ICMPv6 socket
+	ping_socket = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6);
+
+	int val = 2;
+	setsockopt(ping_socket, IPPROTO_RAW, IPV6_CHECKSUM, &val, sizeof(val));
+
+	// This is required by RFC 4861
+	val = 255;
+	setsockopt(ping_socket, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val));
+	setsockopt(ping_socket, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &val, sizeof(val));
+
+	// Filter all packages, we only want to send
+	struct icmp6_filter filt;
+	ICMP6_FILTER_SETBLOCKALL(&filt);
+	setsockopt(ping_socket, IPPROTO_ICMPV6, ICMP6_FILTER, &filt, sizeof(filt));
+
+
+	// Netlink socket, continued...
+	group = RTNLGRP_NEIGH;
+	setsockopt(rtnl_event.uloop.fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group));
+
+	// Synthesize initial neighbor events
+	struct {
+		struct nlmsghdr nh;
+		struct ndmsg ndm;
+	} req = {
+		{sizeof(req), RTM_GETNEIGH, NLM_F_REQUEST | NLM_F_DUMP,
+				++rtnl_seqid, 0},
+		{.ndm_family = AF_INET6}
+	};
+	send(rtnl_event.uloop.fd, &req, sizeof(req), MSG_DONTWAIT);
+
+	return 0;
+}
+
+
+int setup_ndp_interface(struct interface *iface, bool enable)
+{
+	struct packet_mreq mreq = {iface->ifindex, PACKET_MR_ALLMULTI, ETH_ALEN, {0}};
+	setsockopt(ndp_event.uloop.fd, SOL_PACKET, PACKET_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
+
+	struct ndp_neighbor *c, *n;
+	list_for_each_entry_safe(c, n, &neighbors, head)
+		if (c->iface == iface && (c->timeout == 0 || iface->ndp != RELAYD_RELAY || !enable))
+			modify_neighbor(&c->addr, c->iface, false);
+
+	if (enable && iface->ndp == RELAYD_RELAY) {
+		setsockopt(ndp_event.uloop.fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
+
+		if (iface->static_ndp_len) {
+			char *entry = alloca(iface->static_ndp_len), *saveptr;
+			memcpy(entry, iface->static_ndp, iface->static_ndp_len);
+
+			for (entry = strtok_r(entry, " ", &saveptr); entry; entry = strtok_r(NULL, " ", &saveptr)) {
+				struct ndp_neighbor *n = malloc(sizeof(*n));
+				n->iface = iface;
+				n->timeout = 0;
+
+				char ipbuf[INET6_ADDRSTRLEN];
+				if (sscanf(entry, "%45s/%hhu", ipbuf, &n->len) < 2
+						|| n->len > 128 || inet_pton(AF_INET6, ipbuf, &n->addr) != 1) {
+					syslog(LOG_ERR, "Invalid static NDP-prefix %s", entry);
+					return -1;
+				}
+
+				list_add(&n->head, &neighbors);
+			}
+		}
+	}
+
+	return 0;
+}
+
+
+// Send an ICMP-ECHO. This is less for actually pinging but for the
+// neighbor cache to be kept up-to-date.
+static ssize_t ping6(struct in6_addr *addr,
+		const struct interface *iface)
+{
+	struct sockaddr_in6 dest = {AF_INET6, 0, 0, *addr, 0};
+	struct icmp6_hdr echo = {.icmp6_type = ICMP6_ECHO_REQUEST};
+	struct iovec iov = {&echo, sizeof(echo)};
+
+	// Linux seems to not honor IPV6_PKTINFO on raw-sockets, so work around
+	setsockopt(ping_socket, SOL_SOCKET, SO_BINDTODEVICE,
+			iface->ifname, sizeof(iface->ifname));
+	return odhcpd_send(ping_socket, &dest, &iov, 1, iface);
+}
+
+
+// Handle solicitations
+static void handle_solicit(void *addr, void *data, size_t len,
+		struct interface *iface)
+{
+	struct ip6_hdr *ip6 = data;
+	struct nd_neighbor_solicit *req = (struct nd_neighbor_solicit*)&ip6[1];
+	struct sockaddr_ll *ll = addr;
+
+	// Solicitation is for duplicate address detection
+	bool ns_is_dad = IN6_IS_ADDR_UNSPECIFIED(&ip6->ip6_src);
+
+	// Don't forward any non-DAD solicitation for external ifaces
+	// TODO: check if we should even forward DADs for them
+	if (iface->external && !ns_is_dad)
+		return;
+
+	if (len < sizeof(*ip6) + sizeof(*req))
+		return; // Invalid reqicitation
+
+	if (IN6_IS_ADDR_LINKLOCAL(&req->nd_ns_target) ||
+			IN6_IS_ADDR_LOOPBACK(&req->nd_ns_target) ||
+			IN6_IS_ADDR_MULTICAST(&req->nd_ns_target))
+		return; // Invalid target
+
+	char ipbuf[INET6_ADDRSTRLEN];
+	inet_ntop(AF_INET6, &req->nd_ns_target, ipbuf, sizeof(ipbuf));
+	syslog(LOG_NOTICE, "Got a NS for %s", ipbuf);
+
+	uint8_t mac[6];
+	odhcpd_get_mac(iface, mac);
+	if (!memcmp(ll->sll_addr, mac, sizeof(mac)) &&
+			ll->sll_pkttype != PACKET_OUTGOING)
+		return; // Looped back
+
+	time_t now = time(NULL);
+
+	struct ndp_neighbor *n = find_neighbor(&req->nd_ns_target, false);
+	if (n && (n->iface || abs(n->timeout - now) < 5)) {
+		syslog(LOG_NOTICE, "%s is on %s", ipbuf,
+				(n->iface) ? n->iface->ifname : "<pending>");
+		if (!n->iface || n->iface == iface)
+			return;
+
+		// Found on other interface, answer with advertisement
+		struct {
+			struct nd_neighbor_advert body;
+			struct nd_opt_hdr opt_ll_hdr;
+			uint8_t mac[6];
+		} advert = {
+			.body = {
+				.nd_na_hdr = {ND_NEIGHBOR_ADVERT,
+					0, 0, {{0}}},
+				.nd_na_target = req->nd_ns_target,
+			},
+			.opt_ll_hdr = {ND_OPT_TARGET_LINKADDR, 1},
+		};
+
+		memcpy(advert.mac, mac, sizeof(advert.mac));
+		advert.body.nd_na_flags_reserved = ND_NA_FLAG_ROUTER |
+				ND_NA_FLAG_SOLICITED;
+
+		struct sockaddr_in6 dest = {AF_INET6, 0, 0, ALL_IPV6_NODES, 0};
+		if (!ns_is_dad) // If not DAD, then unicast to source
+			dest.sin6_addr = ip6->ip6_src;
+
+		// Linux seems to not honor IPV6_PKTINFO on raw-sockets, so work around
+		setsockopt(ping_socket, SOL_SOCKET, SO_BINDTODEVICE,
+					iface->ifname, sizeof(iface->ifname));
+		struct iovec iov = {&advert, sizeof(advert)};
+		odhcpd_send(ping_socket, &dest, &iov, 1, iface);
+	} else {
+		// Send echo to all other interfaces to see where target is on
+		// This will trigger neighbor discovery which is what we want.
+		// We will observe the neighbor cache to see results.
+
+		ssize_t sent = 0;
+		struct interface *c;
+		list_for_each_entry(c, &interfaces, head)
+			if (iface->ndp == RELAYD_RELAY && iface != c &&
+					(!ns_is_dad || !c->external == false))
+				sent += ping6(&req->nd_ns_target, c);
+
+		if (sent > 0) // Sent a ping, add pending neighbor entry
+			modify_neighbor(&req->nd_ns_target, NULL, true);
+	}
+}
+
+
+void odhcpd_setup_route(const struct in6_addr *addr, int prefixlen,
+		const struct interface *iface, const struct in6_addr *gw, bool add)
+{
+	struct req {
+		struct nlmsghdr nh;
+		struct rtmsg rtm;
+		struct rtattr rta_dst;
+		struct in6_addr dst_addr;
+		struct rtattr rta_oif;
+		uint32_t ifindex;
+		struct rtattr rta_table;
+		uint32_t table;
+		struct rtattr rta_gw;
+		struct in6_addr gw;
+	} req = {
+		{sizeof(req), 0, NLM_F_REQUEST, ++rtnl_seqid, 0},
+		{AF_INET6, prefixlen, 0, 0, 0, 0, 0, 0, 0},
+		{sizeof(struct rtattr) + sizeof(struct in6_addr), RTA_DST},
+		*addr,
+		{sizeof(struct rtattr) + sizeof(uint32_t), RTA_OIF},
+		iface->ifindex,
+		{sizeof(struct rtattr) + sizeof(uint32_t), RTA_TABLE},
+		RT_TABLE_MAIN,
+		{sizeof(struct rtattr) + sizeof(struct in6_addr), RTA_GATEWAY},
+		IN6ADDR_ANY_INIT,
+	};
+
+	if (gw)
+		req.gw = *gw;
+
+	if (add) {
+		req.nh.nlmsg_type = RTM_NEWROUTE;
+		req.nh.nlmsg_flags |= (NLM_F_CREATE | NLM_F_REPLACE);
+		req.rtm.rtm_protocol = RTPROT_BOOT;
+		req.rtm.rtm_scope = (gw) ? RT_SCOPE_UNIVERSE : RT_SCOPE_LINK;
+		req.rtm.rtm_type = RTN_UNICAST;
+	} else {
+		req.nh.nlmsg_type = RTM_DELROUTE;
+		req.rtm.rtm_scope = RT_SCOPE_NOWHERE;
+	}
+
+	size_t reqlen = (gw) ? sizeof(req) : offsetof(struct req, rta_gw);
+	send(rtnl_event.uloop.fd, &req, reqlen, MSG_DONTWAIT);
+}
+
+// Use rtnetlink to modify kernel routes
+static void setup_route(struct in6_addr *addr, struct interface *iface,
+		bool add)
+{
+	char namebuf[INET6_ADDRSTRLEN];
+	inet_ntop(AF_INET6, addr, namebuf, sizeof(namebuf));
+	syslog(LOG_NOTICE, "%s about %s on %s", (add) ? "Learned" : "Forgot",
+			namebuf, (iface) ? iface->ifname : "<pending>");
+
+	if (!iface || !iface->learn_routes)
+		return;
+
+	odhcpd_setup_route(addr, 128, iface, NULL, add);
+}
+
+static void free_neighbor(struct ndp_neighbor *n)
+{
+	setup_route(&n->addr, n->iface, false);
+	list_del(&n->head);
+	free(n);
+	--neighbor_count;
+}
+
+
+static bool match_neighbor(struct ndp_neighbor *n, struct in6_addr *addr)
+{
+	if (n->len <= 32)
+		return ntohl(n->addr.s6_addr32[0]) >> (32 - n->len) ==
+				ntohl(addr->s6_addr32[0]) >> (32 - n->len);
+
+	if (n->addr.s6_addr32[0] != addr->s6_addr32[0])
+		return false;
+
+	if (n->len <= 64)
+		return ntohl(n->addr.s6_addr32[1]) >> (64 - n->len) ==
+				ntohl(addr->s6_addr32[1]) >> (64 - n->len);
+
+	if (n->addr.s6_addr32[1] != addr->s6_addr32[1])
+		return false;
+
+	if (n->len <= 96)
+		return ntohl(n->addr.s6_addr32[2]) >> (96 - n->len) ==
+				ntohl(addr->s6_addr32[2]) >> (96 - n->len);
+
+	if (n->addr.s6_addr32[2] != addr->s6_addr32[2])
+		return false;
+
+	return ntohl(n->addr.s6_addr32[3]) >> (128 - n->len) ==
+			ntohl(addr->s6_addr32[3]) >> (128 - n->len);
+}
+
+
+static struct ndp_neighbor* find_neighbor(struct in6_addr *addr, bool strict)
+{
+	time_t now = time(NULL);
+	struct ndp_neighbor *n, *e;
+	list_for_each_entry_safe(n, e, &neighbors, head) {
+		if ((!strict && match_neighbor(n, addr)) ||
+				(n->len == 128 && IN6_ARE_ADDR_EQUAL(&n->addr, addr)))
+			return n;
+
+		if (!n->iface && abs(n->timeout - now) >= 5)
+			free_neighbor(n);
+	}
+	return NULL;
+}
+
+
+// Modified our own neighbor-entries
+static void modify_neighbor(struct in6_addr *addr,
+		struct interface *iface, bool add)
+{
+	if (!addr || (void*)addr == (void*)iface)
+		return;
+
+	struct ndp_neighbor *n = find_neighbor(addr, true);
+	if (!add) { // Delete action
+		if (n && (!n->iface || n->iface == iface))
+			free_neighbor(n);
+	} else if (!n) { // No entry yet, add one if possible
+		if (neighbor_count >= NDP_MAX_NEIGHBORS ||
+				!(n = malloc(sizeof(*n))))
+			return;
+
+		n->len = 128;
+		n->addr = *addr;
+		n->iface = iface;
+		if (!n->iface)
+			time(&n->timeout);
+		list_add(&n->head, &neighbors);
+		++neighbor_count;
+		setup_route(addr, n->iface, add);
+	} else if (n->iface == iface) {
+		if (!n->iface)
+			time(&n->timeout);
+	} else if (iface && (!n->iface ||
+			(!iface->external && n->iface->external))) {
+		setup_route(addr, n->iface, false);
+		n->iface = iface;
+		setup_route(addr, n->iface, add);
+	}
+	// TODO: In case a host switches interfaces we might want
+	// to set its old neighbor entry to NUD_STALE and ping it
+	// on the old interface to confirm if the MACs match.
+}
+
+
+// Handler for neighbor cache entries from the kernel. This is our source
+// to learn and unlearn hosts on interfaces.
+static void handle_rtnetlink(_unused void *addr, void *data, size_t len,
+		_unused struct interface *iface)
+{
+	for (struct nlmsghdr *nh = data; NLMSG_OK(nh, len);
+			nh = NLMSG_NEXT(nh, len)) {
+		struct rtmsg *rtm = NLMSG_DATA(nh);
+		if ((nh->nlmsg_type == RTM_NEWROUTE ||
+				nh->nlmsg_type == RTM_DELROUTE) &&
+				rtm->rtm_dst_len == 0)
+			raise(SIGUSR1); // Inform about a change in default route
+
+		struct ndmsg *ndm = NLMSG_DATA(nh);
+		struct ifaddrmsg *ifa = NLMSG_DATA(nh);
+		if (nh->nlmsg_type != RTM_NEWNEIGH
+				&& nh->nlmsg_type != RTM_DELNEIGH
+				&& nh->nlmsg_type != RTM_NEWADDR
+				&& nh->nlmsg_type != RTM_DELADDR)
+			continue; // Unrelated message type
+		bool is_addr = (nh->nlmsg_type == RTM_NEWADDR
+				|| nh->nlmsg_type == RTM_DELADDR);
+
+		// Family and ifindex are on the same offset for NEIGH and ADDR
+		if (NLMSG_PAYLOAD(nh, 0) < sizeof(*ndm)
+				|| ndm->ndm_family != AF_INET6)
+			continue; //
+
+		// Lookup interface
+		struct interface *iface;
+		if (!(iface = odhcpd_get_interface_by_index(ndm->ndm_ifindex)))
+			continue;
+
+		// Data to retrieve
+		size_t rta_offset = (is_addr) ? sizeof(*ifa) : sizeof(*ndm);
+		uint16_t atype = (is_addr) ? IFA_ADDRESS : NDA_DST;
+		ssize_t alen = NLMSG_PAYLOAD(nh, rta_offset);
+		struct in6_addr *addr = NULL;
+
+		for (struct rtattr *rta = (void*)(((uint8_t*)ndm) + rta_offset);
+				RTA_OK(rta, alen); rta = RTA_NEXT(rta, alen))
+			if (rta->rta_type == atype &&
+					RTA_PAYLOAD(rta) >= sizeof(*addr))
+				addr = RTA_DATA(rta);
+
+		// Address not specified or unrelated
+		if (!addr || IN6_IS_ADDR_LINKLOCAL(addr) ||
+				IN6_IS_ADDR_MULTICAST(addr))
+			continue;
+
+		// Check for states
+		bool add;
+		if (is_addr)
+			add = (nh->nlmsg_type == RTM_NEWADDR);
+		else
+			add = (nh->nlmsg_type == RTM_NEWNEIGH && (ndm->ndm_state &
+				(NUD_REACHABLE | NUD_STALE | NUD_DELAY | NUD_PROBE
+						| NUD_PERMANENT | NUD_NOARP)));
+
+		if (iface->ndp == RELAYD_RELAY)
+			modify_neighbor(addr, iface, add);
+
+		if (is_addr && iface->ra == RELAYD_SERVER)
+			raise(SIGUSR1); // Inform about a change in addresses
+
+		if (is_addr && iface->dhcpv6 == RELAYD_SERVER)
+			iface->ia_reconf = true;
+
+		if (iface->ndp == RELAYD_RELAY && is_addr && iface->master) {
+			// Replay address changes on all slave interfaces
+			nh->nlmsg_flags = NLM_F_REQUEST;
+
+			if (nh->nlmsg_type == RTM_NEWADDR)
+				nh->nlmsg_flags |= NLM_F_CREATE | NLM_F_REPLACE;
+
+			struct interface *c;
+			list_for_each_entry(c, &interfaces, head) {
+				if (c->ndp == RELAYD_RELAY && !c->master) {
+					ifa->ifa_index = c->ifindex;
+					send(rtnl_event.uloop.fd, nh, nh->nlmsg_len, MSG_DONTWAIT);
+				}
+			}
+		}
+
+		/* TODO: See if this is required for optimal operation
+		// Keep neighbor entries alive so we don't loose routes
+		if (add && (ndm->ndm_state & NUD_STALE))
+			ping6(addr, iface);
+		*/
+	}
+}

+ 31 - 0
src/ndp.h

@@ -0,0 +1,31 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#pragma once
+#include "odhcpd.h"
+#include <time.h>
+
+#ifndef SOL_NETLINK
+#define SOL_NETLINK 270
+#endif
+
+#define NDP_MAX_NEIGHBORS 1000
+
+struct ndp_neighbor {
+	struct list_head head;
+	struct interface *iface;
+	struct in6_addr addr;
+	uint8_t len;
+	time_t timeout;
+};

+ 423 - 0
src/odhcpd.c

@@ -0,0 +1,423 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#include <time.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <resolv.h>
+#include <getopt.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <stdbool.h>
+
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <netinet/ip6.h>
+#include <netpacket/packet.h>
+#include <linux/rtnetlink.h>
+
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <sys/epoll.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/syscall.h>
+
+#include <libubox/uloop.h>
+#include "odhcpd.h"
+
+
+
+static int ioctl_sock;
+static int rtnl_socket = -1;
+static int rtnl_seq = 0;
+static int urandom_fd = -1;
+
+
+int main()
+{
+	openlog("odhcpd", LOG_PERROR | LOG_PID, LOG_DAEMON);
+	uloop_init();
+
+	if (getuid() != 0) {
+		syslog(LOG_ERR, "Must be run as root!");
+		return 2;
+	}
+
+	ioctl_sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
+
+	if ((rtnl_socket = odhcpd_open_rtnl()) < 0) {
+		syslog(LOG_ERR, "Unable to open socket: %s", strerror(errno));
+		return 2;
+	}
+
+	if ((urandom_fd = open("/dev/urandom", O_RDONLY | O_CLOEXEC)) < 0)
+		return 4;
+
+	signal(SIGUSR1, SIG_IGN);
+
+	if (init_router())
+		return 4;
+
+	if (init_dhcpv6())
+		return 4;
+
+	if (init_ndp())
+		return 4;
+
+	if (init_dhcpv4())
+		return 4;
+
+	if (init_ubus())
+		return 4;
+
+	odhcpd_run();
+	return 0;
+}
+
+int odhcpd_open_rtnl(void)
+{
+	int sock = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE);
+
+	// Connect to the kernel netlink interface
+	struct sockaddr_nl nl = {.nl_family = AF_NETLINK};
+	if (connect(sock, (struct sockaddr*)&nl, sizeof(nl))) {
+		syslog(LOG_ERR, "Failed to connect to kernel rtnetlink: %s",
+				strerror(errno));
+		return -1;
+	}
+
+	return sock;
+}
+
+
+// Read IPv6 MTU for interface
+int odhcpd_get_interface_mtu(const char *ifname)
+{
+	char buf[64];
+	const char *sysctl_pattern = "/proc/sys/net/ipv6/conf/%s/mtu";
+	snprintf(buf, sizeof(buf), sysctl_pattern, ifname);
+
+	int fd = open(buf, O_RDONLY);
+	ssize_t len = read(fd, buf, sizeof(buf) - 1);
+	close(fd);
+
+	if (len < 0)
+		return -1;
+
+
+	buf[len] = 0;
+	return atoi(buf);
+
+}
+
+
+// Read IPv6 MAC for interface
+int odhcpd_get_mac(const struct interface *iface, uint8_t mac[6])
+{
+	struct ifreq ifr;
+	strncpy(ifr.ifr_name, iface->ifname, sizeof(ifr.ifr_name));
+	if (ioctl(ioctl_sock, SIOCGIFHWADDR, &ifr) < 0)
+		return -1;
+	memcpy(mac, ifr.ifr_hwaddr.sa_data, 6);
+	return 0;
+}
+
+
+// Forwards a packet on a specific interface
+ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest,
+		struct iovec *iov, size_t iov_len,
+		const struct interface *iface)
+{
+	// Construct headers
+	uint8_t cmsg_buf[CMSG_SPACE(sizeof(struct in6_pktinfo))] = {0};
+	struct msghdr msg = {(void*)dest, sizeof(*dest), iov, iov_len,
+				cmsg_buf, sizeof(cmsg_buf), 0};
+
+	// Set control data (define destination interface)
+	struct cmsghdr *chdr = CMSG_FIRSTHDR(&msg);
+	chdr->cmsg_level = IPPROTO_IPV6;
+	chdr->cmsg_type = IPV6_PKTINFO;
+	chdr->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo));
+	struct in6_pktinfo *pktinfo = (struct in6_pktinfo*)CMSG_DATA(chdr);
+	pktinfo->ipi6_ifindex = iface->ifindex;
+
+	// Also set scope ID if link-local
+	if (IN6_IS_ADDR_LINKLOCAL(&dest->sin6_addr)
+			|| IN6_IS_ADDR_MC_LINKLOCAL(&dest->sin6_addr))
+		dest->sin6_scope_id = iface->ifindex;
+
+	// IPV6_PKTINFO doesn't really work for IPv6-raw sockets (bug?)
+	if (dest->sin6_port == 0) {
+		msg.msg_control = NULL;
+		msg.msg_controllen = 0;
+	}
+
+	char ipbuf[INET6_ADDRSTRLEN];
+	inet_ntop(AF_INET6, &dest->sin6_addr, ipbuf, sizeof(ipbuf));
+
+	ssize_t sent = sendmsg(socket, &msg, MSG_DONTWAIT);
+	if (sent < 0)
+		syslog(LOG_WARNING, "Failed to send to %s%%%s (%s)",
+				ipbuf, iface->ifname, strerror(errno));
+	else
+		syslog(LOG_NOTICE, "Sent %li bytes to %s%%%s",
+				(long)sent, ipbuf, iface->ifname);
+	return sent;
+}
+
+
+// Detect an IPV6-address currently assigned to the given interface
+ssize_t odhcpd_get_interface_addresses(int ifindex,
+		struct odhcpd_ipaddr *addrs, size_t cnt)
+{
+	struct {
+		struct nlmsghdr nhm;
+		struct ifaddrmsg ifa;
+	} req = {{sizeof(req), RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP,
+			++rtnl_seq, 0}, {AF_INET6, 0, 0, 0, ifindex}};
+	if (send(rtnl_socket, &req, sizeof(req), 0) < (ssize_t)sizeof(req))
+		return 0;
+
+	uint8_t buf[8192];
+	ssize_t len = 0, ret = 0;
+
+	for (struct nlmsghdr *nhm = NULL; ; nhm = NLMSG_NEXT(nhm, len)) {
+		while (len < 0 || !NLMSG_OK(nhm, (size_t)len)) {
+			len = recv(rtnl_socket, buf, sizeof(buf), 0);
+			nhm = (struct nlmsghdr*)buf;
+			if (len < 0 || !NLMSG_OK(nhm, (size_t)len)) {
+				if (errno == EINTR)
+					continue;
+				else
+					return ret;
+			}
+		}
+
+		if (nhm->nlmsg_type != RTM_NEWADDR)
+			break;
+
+		// Skip address but keep clearing socket buffer
+		if (ret >= (ssize_t)cnt)
+			continue;
+
+		struct ifaddrmsg *ifa = NLMSG_DATA(nhm);
+		if (ifa->ifa_scope != RT_SCOPE_UNIVERSE ||
+				ifa->ifa_index != (unsigned)ifindex)
+			continue;
+
+		struct rtattr *rta = (struct rtattr*)&ifa[1];
+		size_t alen = NLMSG_PAYLOAD(nhm, sizeof(*ifa));
+		memset(&addrs[ret], 0, sizeof(addrs[ret]));
+		addrs[ret].prefix = ifa->ifa_prefixlen;
+
+		while (RTA_OK(rta, alen)) {
+			if (rta->rta_type == IFA_ADDRESS) {
+				memcpy(&addrs[ret].addr, RTA_DATA(rta),
+						sizeof(struct in6_addr));
+			} else if (rta->rta_type == IFA_CACHEINFO) {
+				struct ifa_cacheinfo *ifc = RTA_DATA(rta);
+				addrs[ret].preferred = ifc->ifa_prefered;
+				addrs[ret].valid = ifc->ifa_valid;
+			}
+
+			rta = RTA_NEXT(rta, alen);
+		}
+
+		if (ifa->ifa_flags & IFA_F_DEPRECATED)
+			addrs[ret].preferred = 0;
+
+		++ret;
+	}
+
+	return ret;
+}
+
+
+struct interface* odhcpd_get_interface_by_index(int ifindex)
+{
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head)
+		if (iface->ifindex == ifindex)
+			return iface;
+
+	return NULL;
+}
+
+
+struct interface* odhcpd_get_interface_by_name(const char *name)
+{
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head)
+		if (!strcmp(iface->ifname, name))
+			return iface;
+
+	return NULL;
+}
+
+
+struct interface* odhcpd_get_master_interface(void)
+{
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head)
+		if (iface->master)
+			return iface;
+
+	return NULL;
+}
+
+
+// Convenience function to receive and do basic validation of packets
+static void odhcpd_receive_packets(struct uloop_fd *u, _unused unsigned int events)
+{
+	struct odhcpd_event *e = container_of(u, struct odhcpd_event, uloop);
+
+	uint8_t data_buf[RELAYD_BUFFER_SIZE], cmsg_buf[128];
+	union {
+		struct sockaddr_in6 in6;
+		struct sockaddr_in in;
+		struct sockaddr_ll ll;
+		struct sockaddr_nl nl;
+	} addr;
+
+	while (true) {
+		struct iovec iov = {data_buf, sizeof(data_buf)};
+		struct msghdr msg = {&addr, sizeof(addr), &iov, 1,
+				cmsg_buf, sizeof(cmsg_buf), 0};
+
+		ssize_t len = recvmsg(u->fd, &msg, MSG_DONTWAIT);
+		if (len < 0) {
+			if (errno == EAGAIN)
+				break;
+			else
+				continue;
+		}
+
+
+		// Extract destination interface
+		int destiface = 0;
+		int *hlim = NULL;
+		struct in6_pktinfo *pktinfo;
+		struct in_pktinfo *pkt4info;
+		for (struct cmsghdr *ch = CMSG_FIRSTHDR(&msg); ch != NULL &&
+				destiface == 0; ch = CMSG_NXTHDR(&msg, ch)) {
+			if (ch->cmsg_level == IPPROTO_IPV6 &&
+					ch->cmsg_type == IPV6_PKTINFO) {
+				pktinfo = (struct in6_pktinfo*)CMSG_DATA(ch);
+				destiface = pktinfo->ipi6_ifindex;
+			} else if (ch->cmsg_level == IPPROTO_IP &&
+					ch->cmsg_type == IP_PKTINFO) {
+				pkt4info = (struct in_pktinfo*)CMSG_DATA(ch);
+				destiface = pkt4info->ipi_ifindex;
+			} else if (ch->cmsg_level == IPPROTO_IPV6 &&
+					ch->cmsg_type == IPV6_HOPLIMIT) {
+				hlim = (int*)CMSG_DATA(ch);
+			}
+		}
+
+		// Check hoplimit if received
+		if (hlim && *hlim != 255)
+			continue;
+
+		// Detect interface for packet sockets
+		if (addr.ll.sll_family == AF_PACKET)
+			destiface = addr.ll.sll_ifindex;
+
+		struct interface *iface =
+				odhcpd_get_interface_by_index(destiface);
+
+		if (!iface && addr.nl.nl_family != AF_NETLINK)
+			continue;
+
+		char ipbuf[INET6_ADDRSTRLEN] = "kernel";
+		if (addr.ll.sll_family == AF_PACKET &&
+				len >= (ssize_t)sizeof(struct ip6_hdr))
+			inet_ntop(AF_INET6, &data_buf[8], ipbuf, sizeof(ipbuf));
+		else if (addr.in6.sin6_family == AF_INET6)
+			inet_ntop(AF_INET6, &addr.in6.sin6_addr, ipbuf, sizeof(ipbuf));
+		else if (addr.in.sin_family == AF_INET)
+			inet_ntop(AF_INET, &addr.in.sin_addr, ipbuf, sizeof(ipbuf));
+
+		syslog(LOG_NOTICE, "--");
+		syslog(LOG_NOTICE, "Received %li Bytes from %s%%%s", (long)len,
+				ipbuf, (iface) ? iface->ifname : "netlink");
+
+		e->handle_dgram(&addr, data_buf, len, iface);
+	}
+}
+
+// Register events for the multiplexer
+int odhcpd_register(struct odhcpd_event *event)
+{
+	event->uloop.cb = odhcpd_receive_packets;
+	return uloop_fd_add(&event->uloop, ULOOP_READ);
+}
+
+void odhcpd_urandom(void *data, size_t len)
+{
+	read(urandom_fd, data, len);
+}
+
+
+time_t odhcpd_time(void)
+{
+	struct timespec ts;
+	syscall(SYS_clock_gettime, CLOCK_MONOTONIC, &ts);
+	return ts.tv_sec;
+}
+
+
+static const char hexdigits[] = "0123456789abcdef";
+static const int8_t hexvals[] = {
+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -1, -1, -2, -1, -1,
+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+     0,  1,  2,  3,  4,  5,  6,  7,  8,  9, -1, -1, -1, -1, -1, -1,
+    -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    -1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+};
+
+ssize_t odhcpd_unhexlify(uint8_t *dst, size_t len, const char *src)
+{
+	size_t c;
+	for (c = 0; c < len && src[0] && src[1]; ++c) {
+		int8_t x = (int8_t)*src++;
+		int8_t y = (int8_t)*src++;
+		if (x < 0 || (x = hexvals[x]) < 0
+				|| y < 0 || (y = hexvals[y]) < 0)
+			return -1;
+		dst[c] = x << 4 | y;
+		while (((int8_t)*src) < 0 ||
+				(*src && hexvals[(uint8_t)*src] < 0))
+			src++;
+	}
+
+	return c;
+}
+
+
+void odhcpd_hexlify(char *dst, const uint8_t *src, size_t len)
+{
+	for (size_t i = 0; i < len; ++i) {
+		*dst++ = hexdigits[src[i] >> 4];
+		*dst++ = hexdigits[src[i] & 0x0f];
+	}
+	*dst = 0;
+}

+ 205 - 0
src/odhcpd.h

@@ -0,0 +1,205 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#pragma once
+#include <netinet/in.h>
+#include <netinet/icmp6.h>
+#include <netinet/ether.h>
+#include <net/if.h>
+#include <stdbool.h>
+#include <syslog.h>
+
+#include "libubox/blobmsg.h"
+
+#ifndef typeof
+#define typeof __typeof
+#endif
+
+#ifndef container_of
+#define container_of(ptr, type, member) (           \
+    (type *)( (char *)ptr - offsetof(type,member) ))
+#endif
+
+#include "libubox/list.h"
+#include "libubox/uloop.h"
+
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+
+// RFC 6106 defines this router advertisement option
+#define ND_OPT_ROUTE_INFO 24
+#define ND_OPT_RECURSIVE_DNS 25
+#define ND_OPT_DNS_SEARCH 31
+
+#define RELAYD_BUFFER_SIZE 8192
+#define RELAYD_MAX_PREFIXES 8
+
+#define _unused __attribute__((unused))
+#define _packed __attribute__((packed))
+
+
+#define ALL_IPV6_NODES {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}}}
+
+#define ALL_IPV6_ROUTERS {{{0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}}}
+
+
+struct interface;
+extern struct list_head leases;
+
+struct odhcpd_event {
+	struct uloop_fd uloop;
+	void (*handle_dgram)(void *addr, void *data, size_t len,
+			struct interface *iface);
+};
+
+
+struct odhcpd_ipaddr {
+	struct in6_addr addr;
+	uint8_t prefix;
+	uint32_t preferred;
+	uint32_t valid;
+};
+
+enum odhcpd_mode {
+	RELAYD_DISABLED,
+	RELAYD_SERVER,
+	RELAYD_RELAY,
+	RELAYD_HYBRID
+};
+
+
+struct config {
+	bool legacy;
+	char *dhcp_cb;
+	char *dhcp_statefile;
+} config;
+
+
+struct lease {
+	struct list_head head;
+	struct in_addr ipaddr;
+	uint32_t hostid;
+	struct ether_addr mac;
+	uint16_t duid_len;
+	uint8_t *duid;
+	char hostname[];
+};
+
+
+struct interface {
+	struct list_head head;
+
+	int ifindex;
+	char ifname[IF_NAMESIZE];
+	char name[IF_NAMESIZE];
+	bool inuse;
+
+	// Runtime data
+	struct uloop_timeout timer_rs;
+	struct list_head ia_assignments;
+	struct odhcpd_ipaddr ia_addr[8];
+	size_t ia_addr_len;
+	bool ia_reconf;
+
+	// DHCPv4
+	struct odhcpd_event dhcpv4_event;
+	struct list_head dhcpv4_assignments;
+
+	// Services
+	enum odhcpd_mode ra;
+	enum odhcpd_mode dhcpv6;
+	enum odhcpd_mode ndp;
+	enum odhcpd_mode dhcpv4;
+
+	// Config
+	bool external;
+	bool master;
+	bool ignore;
+	bool always_rewrite_dns;
+	bool deprecate_ula_if_public_avail;
+	bool ra_not_onlink;
+	bool no_dynamic_dhcp;
+
+	int learn_routes;
+	int default_router;
+	int managed;
+	int route_preference;
+
+	// DHCPv4
+	struct in_addr dhcpv4_start;
+	struct in_addr dhcpv4_end;
+	struct in_addr *dhcpv4_dns;
+	size_t dhcpv4_dns_cnt;
+	uint32_t dhcpv4_leasetime;
+
+	// DNS
+	struct in6_addr *dns;
+	size_t dns_cnt;
+	uint8_t *search;
+	size_t search_len;
+
+	char* static_ndp;
+	size_t static_ndp_len;
+
+	char *upstream;
+	size_t upstream_len;
+};
+
+extern struct list_head interfaces;
+
+#define RELAYD_MANAGED_MFLAG	1
+#define RELAYD_MANAGED_NO_AFLAG	2
+
+
+// Exported main functions
+int odhcpd_open_rtnl(void);
+int odhcpd_register(struct odhcpd_event *event);
+
+ssize_t odhcpd_send(int socket, struct sockaddr_in6 *dest,
+		struct iovec *iov, size_t iov_len,
+		const struct interface *iface);
+ssize_t odhcpd_get_interface_addresses(int ifindex,
+		struct odhcpd_ipaddr *addrs, size_t cnt);
+struct interface* odhcpd_get_interface_by_name(const char *name);
+int odhcpd_get_interface_mtu(const char *ifname);
+int odhcpd_get_mac(const struct interface *iface, uint8_t mac[6]);
+struct interface* odhcpd_get_interface_by_index(int ifindex);
+struct interface* odhcpd_get_master_interface(void);
+void odhcpd_urandom(void *data, size_t len);
+void odhcpd_setup_route(const struct in6_addr *addr, int prefixlen,
+		const struct interface *iface, const struct in6_addr *gw, bool add);
+
+void odhcpd_run(void);
+time_t odhcpd_time(void);
+ssize_t odhcpd_unhexlify(uint8_t *dst, size_t len, const char *src);
+void odhcpd_hexlify(char *dst, const uint8_t *src, size_t len);
+
+int config_parse_interface(struct blob_attr *b, const char *iname);
+
+const char* ubus_get_ifname(const char *name);
+void ubus_apply_network(void);
+
+
+// Exported module initializers
+int init_router(void);
+int init_dhcpv6(void);
+int init_dhcpv4(void);
+int init_ndp(void);
+int init_ubus(void);
+
+int setup_router_interface(struct interface *iface, bool enable);
+int setup_dhcpv6_interface(struct interface *iface, bool enable);
+int setup_ndp_interface(struct interface *iface, bool enable);
+int setup_dhcpv4_interface(struct interface *iface, bool enable);

+ 502 - 0
src/router.c

@@ -0,0 +1,502 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <resolv.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <net/route.h>
+
+#include "router.h"
+#include "odhcpd.h"
+
+
+static void forward_router_solicitation(const struct interface *iface);
+static void forward_router_advertisement(uint8_t *data, size_t len);
+
+static void handle_icmpv6(void *addr, void *data, size_t len,
+		struct interface *iface);
+static void send_router_advert(struct uloop_timeout *event);
+static void sigusr1_refresh(int signal);
+
+static struct odhcpd_event router_event = {{.fd = -1}, handle_icmpv6};
+
+static FILE *fp_route = NULL;
+
+
+int init_router(void)
+{
+	// Open ICMPv6 socket
+	int sock = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6);
+	if (sock < 0 && errno != EAFNOSUPPORT) {
+		syslog(LOG_ERR, "Failed to open RAW-socket: %s", strerror(errno));
+		return -1;
+	}
+
+	// Let the kernel compute our checksums
+	int val = 2;
+	setsockopt(sock, IPPROTO_RAW, IPV6_CHECKSUM, &val, sizeof(val));
+
+	// This is required by RFC 4861
+	val = 255;
+	setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val));
+	setsockopt(sock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &val, sizeof(val));
+
+	// We need to know the source interface
+	val = 1;
+	setsockopt(sock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &val, sizeof(val));
+	setsockopt(sock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &val, sizeof(val));
+
+	// Don't loop back
+	val = 0;
+	setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &val, sizeof(val));
+
+	// Filter ICMPv6 package types
+	struct icmp6_filter filt;
+	ICMP6_FILTER_SETBLOCKALL(&filt);
+	ICMP6_FILTER_SETPASS(ND_ROUTER_ADVERT, &filt);
+	ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filt);
+	setsockopt(sock, IPPROTO_ICMPV6, ICMP6_FILTER, &filt, sizeof(filt));
+
+	// Register socket
+	router_event.uloop.fd = sock;
+	odhcpd_register(&router_event);
+
+	if (!(fp_route = fopen("/proc/net/ipv6_route", "r")))
+		syslog(LOG_ERR, "Failed to open routing table: %s",
+				strerror(errno));
+
+	signal(SIGUSR1, sigusr1_refresh);
+	return 0;
+}
+
+
+int setup_router_interface(struct interface *iface, bool enable)
+{
+	struct ipv6_mreq all_nodes = {ALL_IPV6_NODES, iface->ifindex};
+	struct ipv6_mreq all_routers = {ALL_IPV6_ROUTERS, iface->ifindex};
+
+	uloop_timeout_cancel(&iface->timer_rs);
+	iface->timer_rs.cb = NULL;
+
+	setsockopt(router_event.uloop.fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP,
+			&all_nodes, sizeof(all_nodes));
+	setsockopt(router_event.uloop.fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP,
+			&all_routers, sizeof(all_routers));
+
+	if (!enable) {
+		if (iface->ra)
+			send_router_advert(&iface->timer_rs);
+	} else {
+		void *mreq = &all_routers;
+
+		if (iface->ra == RELAYD_RELAY && iface->master) {
+			mreq = &all_nodes;
+			forward_router_solicitation(iface);
+		} else if (iface->ra == RELAYD_SERVER && !iface->master) {
+			iface->timer_rs.cb = send_router_advert;
+			send_router_advert(&iface->timer_rs);
+		}
+
+		if (iface->ra == RELAYD_RELAY || (iface->ra == RELAYD_SERVER && !iface->master))
+			setsockopt(router_event.uloop.fd, IPPROTO_IPV6,
+					IPV6_ADD_MEMBERSHIP, mreq, sizeof(all_nodes));
+	}
+	return 0;
+}
+
+
+// Signal handler to resend all RDs
+static void sigusr1_refresh(_unused int signal)
+{
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head)
+		if (iface->ra == RELAYD_SERVER && !iface->master)
+			uloop_timeout_set(&iface->timer_rs, 1000);
+}
+
+
+// Event handler for incoming ICMPv6 packets
+static void handle_icmpv6(_unused void *addr, void *data, size_t len,
+		struct interface *iface)
+{
+	struct icmp6_hdr *hdr = data;
+	if ((iface->ra == RELAYD_SERVER && !iface->master)) { // Server mode
+		if (hdr->icmp6_type == ND_ROUTER_SOLICIT)
+			send_router_advert(&iface->timer_rs);
+	} else if (iface->ra == RELAYD_RELAY) { // Relay mode
+		if (hdr->icmp6_type == ND_ROUTER_ADVERT && iface->master)
+			forward_router_advertisement(data, len);
+		else if (hdr->icmp6_type == ND_ROUTER_SOLICIT && !iface->master)
+			forward_router_solicitation(odhcpd_get_master_interface());
+	}
+}
+
+
+static bool match_route(const struct odhcpd_ipaddr *n, const struct in6_addr *addr)
+{
+	if (n->prefix <= 32)
+		return ntohl(n->addr.s6_addr32[0]) >> (32 - n->prefix) ==
+				ntohl(addr->s6_addr32[0]) >> (32 - n->prefix);
+
+	if (n->addr.s6_addr32[0] != addr->s6_addr32[0])
+		return false;
+
+	return ntohl(n->addr.s6_addr32[1]) >> (64 - n->prefix) ==
+			ntohl(addr->s6_addr32[1]) >> (64 - n->prefix);
+}
+
+
+// Detect whether a default route exists, also find the source prefixes
+static bool parse_routes(struct odhcpd_ipaddr *n, ssize_t len)
+{
+	rewind(fp_route);
+
+	char line[512], ifname[16];
+	bool found_default = false;
+	struct odhcpd_ipaddr p = {IN6ADDR_ANY_INIT, 0, 0, 0};
+	while (fgets(line, sizeof(line), fp_route)) {
+		uint32_t rflags;
+		if (sscanf(line, "00000000000000000000000000000000 00 "
+				"%*s %*s %*s %*s %*s %*s %*s %15s", ifname) &&
+				strcmp(ifname, "lo")) {
+			found_default = true;
+		} else if (sscanf(line, "%8" SCNx32 "%8" SCNx32 "%*8" SCNx32 "%*8" SCNx32 " %hhx %*s "
+				"%*s 00000000000000000000000000000000 %*s %*s %*s %" SCNx32 " lo",
+				&p.addr.s6_addr32[0], &p.addr.s6_addr32[1], &p.prefix, &rflags) &&
+				p.prefix > 0 && (rflags & RTF_NONEXTHOP) && (rflags & RTF_REJECT)) {
+			// Find source prefixes by scanning through unreachable-routes
+			p.addr.s6_addr32[0] = htonl(p.addr.s6_addr32[0]);
+			p.addr.s6_addr32[1] = htonl(p.addr.s6_addr32[1]);
+
+			for (ssize_t i = 0; i < len; ++i) {
+				if (n[i].prefix <= 64 && n[i].prefix >= p.prefix &&
+						match_route(&p, &n[i].addr)) {
+					n[i].prefix = p.prefix;
+					break;
+				}
+			}
+
+		}
+	}
+
+	return found_default;
+}
+
+
+// Router Advert server mode
+static void send_router_advert(struct uloop_timeout *event)
+{
+	struct interface *iface =
+			container_of(event, struct interface, timer_rs);
+
+	int mtu = odhcpd_get_interface_mtu(iface->ifname);
+	if (mtu < 0)
+		mtu = 1500;
+
+	struct {
+		struct nd_router_advert h;
+		struct icmpv6_opt lladdr;
+		struct nd_opt_mtu mtu;
+		struct nd_opt_prefix_info prefix[RELAYD_MAX_PREFIXES];
+	} adv = {
+		.h = {{.icmp6_type = ND_ROUTER_ADVERT, .icmp6_code = 0}, 0, 0},
+		.lladdr = {ND_OPT_SOURCE_LINKADDR, 1, {0}},
+		.mtu = {ND_OPT_MTU, 1, 0, htonl(mtu)},
+	};
+	adv.h.nd_ra_flags_reserved = ND_RA_FLAG_OTHER;
+	if (iface->managed >= RELAYD_MANAGED_MFLAG)
+		adv.h.nd_ra_flags_reserved |= ND_RA_FLAG_MANAGED;
+
+	if (iface->route_preference < 0)
+		adv.h.nd_ra_flags_reserved |= ND_RA_PREF_LOW;
+	else if (iface->route_preference > 0)
+		adv.h.nd_ra_flags_reserved |= ND_RA_PREF_HIGH;
+	odhcpd_get_mac(iface, adv.lladdr.data);
+
+	// If not currently shutting down
+	struct odhcpd_ipaddr addrs[RELAYD_MAX_PREFIXES];
+	ssize_t ipcnt = 0;
+
+	// If not shutdown
+	if (event->cb) {
+		ipcnt = odhcpd_get_interface_addresses(iface->ifindex,
+				addrs, ARRAY_SIZE(addrs));
+
+		// Check default route
+		if (parse_routes(addrs, ipcnt) || iface->default_router > 1)
+			adv.h.nd_ra_router_lifetime =
+					htons(3 * MaxRtrAdvInterval);
+	}
+
+	// Construct Prefix Information options
+	bool have_public = false;
+	size_t cnt = 0;
+
+	struct in6_addr *dns_addr = NULL;
+	uint32_t dns_time = 0;
+	size_t dns_cnt = 1;
+
+	for (ssize_t i = 0; i < ipcnt; ++i) {
+		struct odhcpd_ipaddr *addr = &addrs[i];
+		if (addr->prefix > 64)
+			continue; // Address not suitable
+
+		if (addr->preferred > MaxPreferredTime)
+			addr->preferred = MaxPreferredTime;
+
+		if (addr->valid > MaxValidTime)
+			addr->valid = MaxValidTime;
+
+		struct nd_opt_prefix_info *p = NULL;
+		for (size_t i = 0; i < cnt; ++i) {
+			if (!memcmp(&adv.prefix[i].nd_opt_pi_prefix,
+					&addr->addr, 8))
+				p = &adv.prefix[i];
+		}
+
+		if (!p) {
+			if (cnt >= ARRAY_SIZE(adv.prefix))
+				break;
+
+			p = &adv.prefix[cnt++];
+		}
+
+		if ((addr->addr.s6_addr[0] & 0xfe) != 0xfc && addr->preferred > 0)
+			have_public = true;
+
+		memcpy(&p->nd_opt_pi_prefix, &addr->addr, 8);
+		p->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION;
+		p->nd_opt_pi_len = 4;
+		p->nd_opt_pi_prefix_len = 64;
+		p->nd_opt_pi_flags_reserved = 0;
+		if (!iface->ra_not_onlink)
+			p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_ONLINK;
+		if (iface->managed < RELAYD_MANAGED_NO_AFLAG)
+			p->nd_opt_pi_flags_reserved |= ND_OPT_PI_FLAG_AUTO;
+		p->nd_opt_pi_valid_time = htonl(addr->valid);
+		p->nd_opt_pi_preferred_time = htonl(addr->preferred);
+
+		if (addr->preferred > dns_time) {
+			dns_time = addr->preferred;
+			dns_addr = &addr->addr;
+		}
+	}
+
+	if (!have_public && !iface->default_router && adv.h.nd_ra_router_lifetime) {
+		syslog(LOG_WARNING, "A default route is present but there is no public prefix "
+				"on %s thus we don't announce a default route!", iface->ifname);
+		adv.h.nd_ra_router_lifetime = 0;
+	}
+
+	if (have_public && iface->deprecate_ula_if_public_avail)
+		for (size_t i = 0; i < cnt; ++i)
+			if ((adv.prefix[i].nd_opt_pi_prefix.s6_addr[0] & 0xfe) == 0xfc)
+				adv.prefix[i].nd_opt_pi_preferred_time = 0;
+
+	// DNS Recursive DNS
+	if (iface->dns_cnt > 0) {
+		dns_addr = iface->dns;
+		dns_cnt = iface->dns_cnt;
+		dns_time = 2 * MaxRtrAdvInterval;
+	}
+
+	if (!dns_addr)
+		dns_cnt = 0;
+
+	struct {
+		uint8_t type;
+		uint8_t len;
+		uint8_t pad;
+		uint8_t pad2;
+		uint32_t lifetime;
+	} dns = {ND_OPT_RECURSIVE_DNS, (1 + (2 * dns_cnt)), 0, 0, htonl(dns_time)};
+
+
+
+	// DNS Search options
+	uint8_t search_buf[256], *search_domain = iface->search;
+	size_t search_len = iface->search_len, search_padded = 0;
+
+	if (!search_domain && !res_init() && _res.dnsrch[0] && _res.dnsrch[0][0]) {
+		int len = dn_comp(_res.dnsrch[0], search_buf,
+				sizeof(search_buf), NULL, NULL);
+		if (len > 0) {
+			search_domain = search_buf;
+			search_len = len;
+		}
+	}
+
+	if (search_len > 0)
+		search_padded = ((search_len + 7) & (~7)) + 8;
+
+	struct {
+		uint8_t type;
+		uint8_t len;
+		uint8_t pad;
+		uint8_t pad2;
+		uint32_t lifetime;
+		uint8_t name[];
+	} *search = alloca(sizeof(*search) + search_padded);
+	search->type = ND_OPT_DNS_SEARCH;
+	search->len = search_len ? ((sizeof(*search) + search_padded) / 8) : 0;
+	search->pad = 0;
+	search->pad2 = 0;
+	search->lifetime = htonl(2 * MaxRtrAdvInterval);;
+	memcpy(search->name, search_domain, search_len);
+	memset(&search->name[search_len], 0, search_padded - search_len);
+
+
+	size_t routes_cnt = 0;
+	struct {
+		uint8_t type;
+		uint8_t len;
+		uint8_t prefix;
+		uint8_t flags;
+		uint32_t lifetime;
+		uint32_t addr[4];
+	} routes[RELAYD_MAX_PREFIXES];
+
+	for (ssize_t i = 0; i < ipcnt; ++i) {
+		struct odhcpd_ipaddr *addr = &addrs[i];
+		if (addr->prefix > 64 || addr->prefix == 0) {
+			continue; // Address not suitable
+		} else if (addr->prefix > 32) {
+			addr->addr.s6_addr32[1] &= htonl(~((1U << (64 - addr->prefix)) - 1));
+		} else if (addr->prefix <= 32) {
+			addr->addr.s6_addr32[0] &= htonl(~((1U << (32 - addr->prefix)) - 1));
+			addr->addr.s6_addr32[1] = 0;
+		}
+
+		routes[routes_cnt].type = ND_OPT_ROUTE_INFO;
+		routes[routes_cnt].len = sizeof(*routes) / 8;
+		routes[routes_cnt].prefix = addr->prefix;
+		routes[routes_cnt].flags = 0;
+		if (iface->route_preference < 0)
+			routes[routes_cnt].flags |= ND_RA_PREF_LOW;
+		else if (iface->route_preference > 0)
+			routes[routes_cnt].flags |= ND_RA_PREF_HIGH;
+		routes[routes_cnt].lifetime = htonl(addr->valid);
+		routes[routes_cnt].addr[0] = addr->addr.s6_addr32[0];
+		routes[routes_cnt].addr[1] = addr->addr.s6_addr32[1];
+		routes[routes_cnt].addr[2] = addr->addr.s6_addr32[2];
+		routes[routes_cnt].addr[3] = addr->addr.s6_addr32[3];
+
+		++routes_cnt;
+	}
+
+
+	struct iovec iov[] = {{&adv, (uint8_t*)&adv.prefix[cnt] - (uint8_t*)&adv},
+			{&routes, routes_cnt * sizeof(*routes)},
+			{&dns, (dns_cnt) ? sizeof(dns) : 0},
+			{dns_addr, dns_cnt * sizeof(*dns_addr)},
+			{search, search->len * 8}};
+	struct sockaddr_in6 all_nodes = {AF_INET6, 0, 0, ALL_IPV6_NODES, 0};
+	odhcpd_send(router_event.uloop.fd,
+			&all_nodes, iov, ARRAY_SIZE(iov), iface);
+
+	// Rearm timer
+	int msecs;
+	odhcpd_urandom(&msecs, sizeof(msecs));
+	msecs = (labs(msecs) % (1000 * (MaxRtrAdvInterval
+			- MinRtrAdvInterval))) + (MinRtrAdvInterval * 1000);
+	uloop_timeout_set(&iface->timer_rs, msecs);
+}
+
+
+// Forward router solicitation
+static void forward_router_solicitation(const struct interface *iface)
+{
+	if (!iface)
+		return;
+
+	struct icmp6_hdr rs = {ND_ROUTER_SOLICIT, 0, 0, {{0}}};
+	struct iovec iov = {&rs, sizeof(rs)};
+	struct sockaddr_in6 all_routers =
+		{AF_INET6, 0, 0, ALL_IPV6_ROUTERS, iface->ifindex};
+
+	syslog(LOG_NOTICE, "Sending RS to %s", iface->ifname);
+	odhcpd_send(router_event.uloop.fd, &all_routers, &iov, 1, iface);
+}
+
+
+// Handler for incoming router solicitations on slave interfaces
+static void forward_router_advertisement(uint8_t *data, size_t len)
+{
+	struct nd_router_advert *adv = (struct nd_router_advert *)data;
+
+	// Rewrite options
+	uint8_t *end = data + len;
+	uint8_t *mac_ptr = NULL;
+	struct in6_addr *dns_ptr = NULL;
+	size_t dns_count = 0;
+
+	struct icmpv6_opt *opt;
+	icmpv6_for_each_option(opt, &adv[1], end) {
+		if (opt->type == ND_OPT_SOURCE_LINKADDR) {
+			// Store address of source MAC-address
+			mac_ptr = opt->data;
+		} else if (opt->type == ND_OPT_RECURSIVE_DNS && opt->len > 1) {
+			// Check if we have to rewrite DNS
+			dns_ptr = (struct in6_addr*)&opt->data[6];
+			dns_count = (opt->len - 1) / 2;
+		}
+	}
+
+	syslog(LOG_NOTICE, "Got a RA");
+
+	// Indicate a proxy, however we don't follow the rest of RFC 4389 yet
+	adv->nd_ra_flags_reserved |= ND_RA_FLAG_PROXY;
+
+	// Forward advertisement to all slave interfaces
+	struct sockaddr_in6 all_nodes = {AF_INET6, 0, 0, ALL_IPV6_NODES, 0};
+	struct iovec iov = {data, len};
+
+	struct odhcpd_ipaddr addr;
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head) {
+		if (iface->ra != RELAYD_RELAY || iface->master)
+			continue;
+
+		// Fixup source hardware address option
+		if (mac_ptr)
+			odhcpd_get_mac(iface, mac_ptr);
+
+		// If we have to rewrite DNS entries
+		if (iface->always_rewrite_dns && dns_ptr && dns_count > 0) {
+			const struct in6_addr *rewrite = iface->dns;
+			size_t rewrite_cnt = iface->dns_cnt;
+
+			if (rewrite_cnt == 0) {
+				if (odhcpd_get_interface_addresses(iface->ifindex, &addr, 1) < 1)
+					continue; // Unable to comply
+
+				rewrite = &addr.addr;
+				rewrite_cnt = 1;
+			}
+
+			// Copy over any other addresses
+			for (size_t i = 0; i < dns_count; ++i) {
+				size_t j = (i < rewrite_cnt) ? i : rewrite_cnt - 1;
+				dns_ptr[i] = rewrite[j];
+			}
+		}
+
+		odhcpd_send(router_event.uloop.fd, &all_nodes, &iov, 1, iface);
+	}
+}

+ 40 - 0
src/router.h

@@ -0,0 +1,40 @@
+/**
+ * Copyright (C) 2012-2013 Steven Barth <steven@midlink.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License v2 as published by
+ * the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ */
+
+#pragma once
+#include <stdint.h>
+#include <netinet/in.h>
+#include <netinet/icmp6.h>
+
+struct icmpv6_opt {
+	uint8_t type;
+	uint8_t len;
+	uint8_t data[6];
+};
+
+
+#define icmpv6_for_each_option(opt, start, end)\
+	for (opt = (struct icmpv6_opt*)(start);\
+	(void*)(opt + 1) <= (void*)(end) && opt->len > 0 &&\
+	(void*)(opt + opt->len) <= (void*)(end); opt += opt->len)
+
+
+#define MaxRtrAdvInterval 600
+#define MinRtrAdvInterval (MaxRtrAdvInterval / 3)
+#define MaxValidTime 7200
+#define MaxPreferredTime  (3 * MaxRtrAdvInterval)
+
+#define ND_RA_FLAG_PROXY	0x4
+#define ND_RA_PREF_HIGH	(1 << 3)
+#define ND_RA_PREF_LOW		(3 << 3)

+ 314 - 0
src/ubus.c

@@ -0,0 +1,314 @@
+#include <syslog.h>
+#include <libubus.h>
+#include <libubox/uloop.h>
+#include <arpa/inet.h>
+
+#include "odhcpd.h"
+#include "dhcpv6.h"
+#include "dhcpv4.h"
+
+
+static struct ubus_context *ubus = NULL;
+static struct ubus_subscriber netifd;
+static struct blob_buf b;
+static struct blob_attr *dump = NULL;
+static uint32_t objid = 0;
+
+
+static int handle_dhcpv4_leases(struct ubus_context *ctx, _unused struct ubus_object *obj,
+		struct ubus_request_data *req, _unused const char *method,
+		_unused struct blob_attr *msg)
+{
+	blob_buf_init(&b, 0);
+	void *a = blobmsg_open_table(&b, "device");
+	time_t now = odhcpd_time();
+
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head) {
+		if (iface->dhcpv4 != RELAYD_SERVER)
+			continue;
+
+		void *i = blobmsg_open_table(&b, iface->ifname);
+		void *j = blobmsg_open_array(&b, "leases");
+
+		struct dhcpv4_assignment *lease;
+		list_for_each_entry(lease, &iface->dhcpv4_assignments, head) {
+			if (lease->valid_until < now)
+				continue;
+
+			void *l = blobmsg_open_table(&b, NULL);
+
+			char *buf = blobmsg_alloc_string_buffer(&b, "mac", 13);
+			odhcpd_hexlify(buf, lease->hwaddr, sizeof(lease->hwaddr));
+			blobmsg_add_string_buffer(&b);
+
+			blobmsg_add_string(&b, "hostname", lease->hostname);
+
+			buf = blobmsg_alloc_string_buffer(&b, "ip", INET_ADDRSTRLEN);
+			struct in_addr addr = {htonl(lease->addr)};
+			inet_ntop(AF_INET, &addr, buf, INET_ADDRSTRLEN);
+			blobmsg_add_string_buffer(&b);
+
+			blobmsg_add_u32(&b, "valid", now - lease->valid_until);
+
+			blobmsg_close_table(&b, l);
+		}
+
+		blobmsg_close_array(&b, j);
+		blobmsg_close_table(&b, i);
+	}
+
+	blobmsg_close_table(&b, a);
+	ubus_send_reply(ctx, req, b.head);
+	return 0;
+}
+
+
+static int handle_dhcpv6_leases(_unused struct ubus_context *ctx, _unused struct ubus_object *obj,
+		_unused struct ubus_request_data *req, _unused const char *method,
+		_unused struct blob_attr *msg)
+{
+	blob_buf_init(&b, 0);
+	void *a = blobmsg_open_table(&b, "device");
+	time_t now = odhcpd_time();
+
+	struct interface *iface;
+	list_for_each_entry(iface, &interfaces, head) {
+		if (iface->dhcpv6 != RELAYD_SERVER)
+			continue;
+
+		void *i = blobmsg_open_table(&b, iface->ifname);
+		void *j = blobmsg_open_array(&b, "leases");
+
+		struct dhcpv6_assignment *lease;
+		list_for_each_entry(lease, &iface->ia_assignments, head) {
+			if (lease->valid_until < now)
+				continue;
+
+			void *l = blobmsg_open_table(&b, NULL);
+
+			char *buf = blobmsg_alloc_string_buffer(&b, "duid", 264);
+			odhcpd_hexlify(buf, lease->clid_data, lease->clid_len);
+			blobmsg_add_string_buffer(&b);
+
+			blobmsg_add_u32(&b, "iaid", ntohl(lease->iaid));
+			blobmsg_add_string(&b, "hostname", (lease->hostname) ? lease->hostname : "");
+			blobmsg_add_u32(&b, "assigned", lease->assigned);
+			blobmsg_add_u32(&b, "length", lease->length);
+
+			void *m = blobmsg_open_array(&b, "ipv6");
+			struct in6_addr addr;
+			for (size_t i = 0; i < iface->ia_addr_len; ++i) {
+				if (iface->ia_addr[i].prefix > 64)
+					continue;
+
+				addr = iface->ia_addr[i].addr;
+				if (lease->length == 128)
+					addr.s6_addr32[3] = htonl(lease->assigned);
+				else
+					addr.s6_addr32[1] |= htonl(lease->assigned);
+
+				char *c = blobmsg_alloc_string_buffer(&b, NULL, INET6_ADDRSTRLEN);
+				inet_ntop(AF_INET6, &addr, c, INET6_ADDRSTRLEN);
+				blobmsg_add_string_buffer(&b);
+			}
+			blobmsg_close_table(&b, m);
+
+			blobmsg_add_u32(&b, "valid", now - lease->valid_until);
+
+			blobmsg_close_table(&b, l);
+		}
+
+		blobmsg_close_array(&b, j);
+		blobmsg_close_table(&b, i);
+	}
+
+	blobmsg_close_table(&b, a);
+	ubus_send_reply(ctx, req, b.head);
+	return 0;
+}
+
+
+static struct ubus_method main_object_methods[] = {
+	{.name = "ipv4leases", .handler = handle_dhcpv4_leases},
+	{.name = "ipv6leases", .handler = handle_dhcpv6_leases},
+};
+
+static struct ubus_object_type main_object_type =
+		UBUS_OBJECT_TYPE("dhcp", main_object_methods);
+
+static struct ubus_object main_object = {
+        .name = "dhcp",
+        .type = &main_object_type,
+        .methods = main_object_methods,
+        .n_methods = ARRAY_SIZE(main_object_methods),
+};
+
+
+enum {
+	DUMP_ATTR_INTERFACE,
+	DUMP_ATTR_MAX
+};
+
+static const struct blobmsg_policy dump_attrs[DUMP_ATTR_MAX] = {
+	[DUMP_ATTR_INTERFACE] = { .name = "interface", .type = BLOBMSG_TYPE_ARRAY },
+};
+
+
+enum {
+	IFACE_ATTR_INTERFACE,
+	IFACE_ATTR_IFNAME,
+	IFACE_ATTR_UP,
+	IFACE_ATTR_DATA,
+	IFACE_ATTR_MAX,
+};
+
+static const struct blobmsg_policy iface_attrs[IFACE_ATTR_MAX] = {
+	[IFACE_ATTR_INTERFACE] = { .name = "interface", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_IFNAME] = { .name = "ifname", .type = BLOBMSG_TYPE_STRING },
+	[IFACE_ATTR_UP] = { .name = "up", .type = BLOBMSG_TYPE_BOOL },
+	[IFACE_ATTR_DATA] = { .name = "data", .type = BLOBMSG_TYPE_TABLE },
+};
+
+static void handle_dump(_unused struct ubus_request *req, _unused int type, struct blob_attr *msg)
+{
+	struct blob_attr *tb[DUMP_ATTR_INTERFACE];
+	blobmsg_parse(dump_attrs, DUMP_ATTR_MAX, tb, blob_data(msg), blob_len(msg));
+
+	if (!tb[DUMP_ATTR_INTERFACE])
+		return;
+
+	free(dump);
+	dump = blob_memdup(tb[DUMP_ATTR_INTERFACE]);
+	raise(SIGHUP);
+}
+
+
+static struct interface* find_interface(struct blob_attr *msg)
+{
+	struct blob_attr *tb[IFACE_ATTR_MAX];
+	blobmsg_parse(iface_attrs, IFACE_ATTR_MAX, tb, blob_data(msg), blob_len(msg));
+
+	const char *interface = (tb[IFACE_ATTR_INTERFACE]) ?
+			blobmsg_get_string(tb[IFACE_ATTR_INTERFACE]) : "";
+	const char *ifname = (tb[IFACE_ATTR_IFNAME]) ?
+			blobmsg_get_string(tb[IFACE_ATTR_IFNAME]) : "";
+
+	struct interface *c;
+	list_for_each_entry(c, &interfaces, head)
+		if (!strcmp(interface, c->name) || !strcmp(ifname, c->ifname))
+			return c;
+
+	return NULL;
+}
+
+
+static int handle_update(_unused struct ubus_context *ctx, _unused struct ubus_object *obj,
+		_unused struct ubus_request_data *req, _unused const char *method,
+		struct blob_attr *msg)
+{
+	struct interface *iface = find_interface(msg);
+	if (iface && iface->ignore)
+		return 0;
+
+	ubus_invoke(ubus, objid, "dump", NULL, handle_dump, NULL, 0);
+	return 0;
+}
+
+
+static void subscribe_netifd(void)
+{
+	netifd.cb = handle_update;
+	ubus_subscribe(ubus, &netifd, objid);
+	ubus_invoke(ubus, objid, "dump", NULL, handle_dump, NULL, 0);
+}
+
+
+void ubus_apply_network(void)
+{
+	struct blob_attr *c;
+	int rem;
+
+	if (!dump)
+		return;
+
+	blobmsg_for_each_attr(c, dump, rem) {
+		struct interface *iface = find_interface(c);
+		if (!iface || !iface->ignore)
+			config_parse_interface(c, NULL);
+	}
+}
+
+
+enum {
+	OBJ_ATTR_ID,
+	OBJ_ATTR_PATH,
+	OBJ_ATTR_MAX
+};
+
+static const struct blobmsg_policy obj_attrs[OBJ_ATTR_MAX] = {
+	[OBJ_ATTR_ID] = { .name = "id", .type = BLOBMSG_TYPE_INT32 },
+	[OBJ_ATTR_PATH] = { .name = "path", .type = BLOBMSG_TYPE_STRING },
+};
+
+
+static void handle_event(_unused struct ubus_context *ctx, _unused struct ubus_event_handler *ev,
+                _unused const char *type, struct blob_attr *msg)
+{
+	struct blob_attr *tb[OBJ_ATTR_MAX];
+	blobmsg_parse(obj_attrs, OBJ_ATTR_MAX, tb, blob_data(msg), blob_len(msg));
+	objid = 0;
+
+	if (!tb[OBJ_ATTR_ID] || !tb[OBJ_ATTR_PATH])
+		return;
+
+	if (strcmp(blobmsg_get_string(tb[OBJ_ATTR_PATH]), "network.interface"))
+		return;
+
+	objid = blobmsg_get_u32(tb[OBJ_ATTR_ID]);
+	subscribe_netifd();
+}
+
+static struct ubus_event_handler event_handler = { .cb = handle_event };
+
+
+const char* ubus_get_ifname(const char *name)
+{
+	struct blob_attr *c;
+	int rem;
+
+	if (!dump)
+		return NULL;
+
+	blobmsg_for_each_attr(c, dump, rem) {
+		struct blob_attr *tb[IFACE_ATTR_MAX];
+		blobmsg_parse(iface_attrs, IFACE_ATTR_MAX, tb, blob_data(c), blob_len(c));
+
+		if (!tb[IFACE_ATTR_INTERFACE] || strcmp(name,
+				blobmsg_get_string(tb[IFACE_ATTR_INTERFACE])))
+			continue;
+
+		if (tb[IFACE_ATTR_IFNAME])
+			return blobmsg_get_string(tb[IFACE_ATTR_IFNAME]);
+	}
+
+	return NULL;
+}
+
+
+int init_ubus(void)
+{
+	if (!(ubus = ubus_connect(NULL))) {
+		syslog(LOG_ERR, "Unable to connect to ubus: %s", strerror(errno));
+		return -1;
+	}
+
+	ubus_add_uloop(ubus);
+	ubus_add_object(ubus, &main_object);
+	ubus_register_event_handler(ubus, &event_handler, "ubus.object.add");
+	if (!ubus_lookup_id(ubus, "network.interface", &objid))
+		subscribe_netifd();
+
+	return 0;
+}
+