From c20a7603ebe7f3246901e85d22ccc6476bcfbc96 Mon Sep 17 00:00:00 2001 From: Anatole Denis Date: Tue, 17 Sep 2019 11:18:47 +0200 Subject: [PATCH 1/3] server6: Fix listening on multicast addresses Joining a multicast group with an address that can't be received on a socket is ineffective, at least on linux. This updates the logic of NewServer in a mostly backwards-compatible way, to enable listening on arbitrary multicast addresses: * Unicast addresses see no user-visible change, but don't join a multicast group for which they don't receive traffic anyway * Multicast addresses start actually receiving traffic for the group they represent, and don't join the default group. **this is a behaviour change**: previously they would receive traffic for the default group if it was on the same port and **not** for the group they represent. I consider that previous behaviour a bug * Wildcard addresses, if on the proper port, will join both AllDHCPRelayAgentsAndServers and AllDHCPServers **this is a behaviour change**: previously only AllDHCPRelayAgentsAndServers was joined * Wildcard addresses on another port: no visible change, same as unicast case Signed-off-by: Anatole Denis --- dhcpv6/server6/server.go | 53 ++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/dhcpv6/server6/server.go b/dhcpv6/server6/server.go index ab9cb328..4506195b 100644 --- a/dhcpv6/server6/server.go +++ b/dhcpv6/server6/server.go @@ -126,31 +126,46 @@ func NewServer(ifname string, addr *net.UDPAddr, handler Handler, opt ...ServerO o(s) } - if s.conn == nil { - // no connection provided by the user, create a new one - conn, err := net.ListenUDP("udp6", addr) + if s.conn != nil { + return s, nil + } + + var err error + // no connection provided by the user, create a new one + s.conn, err = net.ListenUDP("udp6", addr) + if err != nil { + return nil, err + } + + var iface *net.Interface + if ifname == "" { + iface = nil + } else { + iface, err = net.InterfaceByName(ifname) if err != nil { return nil, err } - // join multicast group on the specified interface - var iface *net.Interface - if ifname == "" { - iface = nil - } else { - iface, err = net.InterfaceByName(ifname) - if err != nil { + } + + p := ipv6.NewPacketConn(s.conn) + if addr.IP.IsMulticast() { + if err := p.JoinGroup(iface, addr); err != nil { + return nil, err + } + } else if addr.IP.IsUnspecified() && addr.Port == dhcpv6.DefaultServerPort { + // For wildcard addresses on the correct port, listen on both multicast + // addresses defined in the RFC as a "default" behaviour + for _, g := range []net.IP{dhcpv6.AllDHCPRelayAgentsAndServers, dhcpv6.AllDHCPServers} { + group := net.UDPAddr{ + IP: g, + Port: dhcpv6.DefaultServerPort, + } + if err := p.JoinGroup(iface, &group); err != nil { return nil, err } + } - group := net.UDPAddr{ - IP: dhcpv6.AllDHCPRelayAgentsAndServers, - Port: dhcpv6.DefaultServerPort, - } - p := ipv6.NewPacketConn(conn) - if err := p.JoinGroup(iface, &group); err != nil { - return nil, err - } - s.conn = conn } + return s, nil } From eac77666371b0272d980e9ea739cc890957971a9 Mon Sep 17 00:00:00 2001 From: Anatole Denis Date: Tue, 17 Sep 2019 11:41:54 +0200 Subject: [PATCH 2/3] dhcpv4: Move BindToInterface to interfaces package This moves the implementations of the BindToInterface to the interfaces/ package, since they aren't ipv4-specific. The BindToInterface function remains in dhcpv4 (simply wraps the one in interfaces) to keep backwards-compatibility Additionally, fold bindtodevice_darwin into bindtodevice_bsd: darwin is mostly a BSD, and happens to support IP_RECVIF, so use that instead of IP_BOUND_IF, which only affects sends, not receives according to the code comments in bsd/netinet/ip_output.c as well as being v4-only Signed-off-by: Anatole Denis --- dhcpv4/bindtodevice_darwin.go | 19 ------------------- dhcpv4/bindtointerface.go | 10 ++++++++++ {dhcpv4 => interfaces}/bindtodevice_bsd.go | 4 ++-- {dhcpv4 => interfaces}/bindtodevice_linux.go | 2 +- 4 files changed, 13 insertions(+), 22 deletions(-) delete mode 100644 dhcpv4/bindtodevice_darwin.go create mode 100644 dhcpv4/bindtointerface.go rename {dhcpv4 => interfaces}/bindtodevice_bsd.go (85%) rename {dhcpv4 => interfaces}/bindtodevice_linux.go (88%) diff --git a/dhcpv4/bindtodevice_darwin.go b/dhcpv4/bindtodevice_darwin.go deleted file mode 100644 index 3a1406b6..00000000 --- a/dhcpv4/bindtodevice_darwin.go +++ /dev/null @@ -1,19 +0,0 @@ -// +build darwin - -package dhcpv4 - -import ( - "net" - - "golang.org/x/sys/unix" -) - -// BindToInterface emulates linux's SO_BINDTODEVICE option for a socket by using -// SO_IP_BOUND_IF. -func BindToInterface(fd int, ifname string) error { - iface, err := net.InterfaceByName(ifname) - if err != nil { - return err - } - return unix.SetsockoptInt(fd, unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index) -} diff --git a/dhcpv4/bindtointerface.go b/dhcpv4/bindtointerface.go new file mode 100644 index 00000000..dbe8fbcd --- /dev/null +++ b/dhcpv4/bindtointerface.go @@ -0,0 +1,10 @@ +package dhcpv4 + +import ( + "github.com/insomniacslk/dhcp/interfaces" +) + +// BindToInterface (deprecated) redirects to interfaces.BindToInterface +func BindToInterface(fd int, ifname string) error { + return interfaces.BindToInterface(fd, ifname) +} diff --git a/dhcpv4/bindtodevice_bsd.go b/interfaces/bindtodevice_bsd.go similarity index 85% rename from dhcpv4/bindtodevice_bsd.go rename to interfaces/bindtodevice_bsd.go index 0f1b5819..180c3eba 100644 --- a/dhcpv4/bindtodevice_bsd.go +++ b/interfaces/bindtodevice_bsd.go @@ -1,6 +1,6 @@ -// +build freebsd openbsd netbsd +// +build freebsd openbsd netbsd darwin -package dhcpv4 +package interfaces import ( "net" diff --git a/dhcpv4/bindtodevice_linux.go b/interfaces/bindtodevice_linux.go similarity index 88% rename from dhcpv4/bindtodevice_linux.go rename to interfaces/bindtodevice_linux.go index c1ae025d..52c7177b 100644 --- a/dhcpv4/bindtodevice_linux.go +++ b/interfaces/bindtodevice_linux.go @@ -1,6 +1,6 @@ // +build linux -package dhcpv4 +package interfaces import "golang.org/x/sys/unix" From a8311dfaca587339aa1d905598ac0f2093ad7ea9 Mon Sep 17 00:00:00 2001 From: Anatole Denis Date: Tue, 17 Sep 2019 16:02:29 +0200 Subject: [PATCH 3/3] server6: Create UDP conn manually for more control Similar to server4 where the UDP connection is manually created using the socket interfaces, this creates a connection with adequate options: * SO_BINDTODEVICE or equivalent if an interface is requested * V6ONLY when supported by the operating system * Allows binding to a multicast address specifically instead of falling back to wildcard Signed-off-by: Anatole Denis --- dhcpv6/server6/conn.go | 62 +++++++++++++++++++++++++++++++++++ dhcpv6/server6/server.go | 32 ++++++++++++------ dhcpv6/server6/server_test.go | 2 +- 3 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 dhcpv6/server6/conn.go diff --git a/dhcpv6/server6/conn.go b/dhcpv6/server6/conn.go new file mode 100644 index 00000000..08c54e89 --- /dev/null +++ b/dhcpv6/server6/conn.go @@ -0,0 +1,62 @@ +package server6 + +import ( + "errors" + "fmt" + "net" + "os" + + "github.com/insomniacslk/dhcp/interfaces" + "golang.org/x/sys/unix" +) + +// NewIPv6UDPConn returns a UDPv6-only connection bound to both the interface and port +// given based on a IPv6 DGRAM socket. +// As a bonus, you can actually listen on a multicast address instead of being punted to the wildcard +// +// The interface must already be configured. +func NewIPv6UDPConn(iface string, addr *net.UDPAddr) (net.PacketConn, error) { + fd, err := unix.Socket(unix.AF_INET6, unix.SOCK_DGRAM, unix.IPPROTO_UDP) + if err != nil { + return nil, fmt.Errorf("cannot get a UDP socket: %v", err) + } + f := os.NewFile(uintptr(fd), "") + // net.FilePacketConn dups the FD, so we have to close this in any case. + defer f.Close() + + // Allow broadcasting. + if err := unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_V6ONLY, 1); err != nil { + if errno, ok := err.(unix.Errno); !ok { + return nil, fmt.Errorf("unexpected socket error: %v", err) + } else if errno != unix.ENOPROTOOPT { // Unsupported on some OSes (but in that case v6only is default), so we ignore ENOPROTOOPT + return nil, fmt.Errorf("cannot bind socket v6only %v", err) + } + } + // Allow reusing the addr to aid debugging. + if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil { + return nil, fmt.Errorf("cannot set reuseaddr on socket: %v", err) + } + if len(iface) != 0 { + // Bind directly to the interface. + if err := interfaces.BindToInterface(fd, iface); err != nil { + if errno, ok := err.(unix.Errno); ok && errno == unix.EACCES { + // Return a more helpful error message in this (fairly common) case + return nil, errors.New("Cannot bind to interface without CAP_NET_RAW or root permissions. " + + "Restart with elevated privilege, or run without specifying an interface to bind to all available interfaces.") + } + return nil, fmt.Errorf("cannot bind to interface %s: %v", iface, err) + } + } + + if addr == nil { + return nil, errors.New("An address to listen on needs to be specified") + } + // Bind to the port. + saddr := unix.SockaddrInet6{Port: addr.Port} + copy(saddr.Addr[:], addr.IP) + if err := unix.Bind(fd, &saddr); err != nil { + return nil, fmt.Errorf("cannot bind to address %v: %v", addr, err) + } + + return net.FilePacketConn(f) +} diff --git a/dhcpv6/server6/server.go b/dhcpv6/server6/server.go index 4506195b..ac250768 100644 --- a/dhcpv6/server6/server.go +++ b/dhcpv6/server6/server.go @@ -112,9 +112,14 @@ func WithConn(conn net.PacketConn) ServerOpt { } } -// NewServer initializes and returns a new Server object, listening on `addr`, -// and joining the multicast group ff02::1:2 . If `addr` is nil, IPv6 unspec is -// used. If `WithConn` is used with a non-nil address, `addr` and `ifname` have +// NewServer initializes and returns a new Server object, listening on `addr`. +// * If `addr` is a multicast group, the group will be additionally joined +// * If `addr` is the wildcard address on the DHCPv6 server port (`[::]:547), the +// multicast groups All_DHCP_Relay_Agents_and_Servers(`[ff02::1:2]`) and +// All_DHCP_Servers(`[ff05::1:3]:547`) will be joined. +// * If `addr` is nil, IPv6 unspec on the DHCP server port is used and the above +// behaviour applies +// If `WithConn` is used with a non-nil address, `addr` and `ifname` have // no effect. In such case, joining the multicast group is the caller's // responsibility. func NewServer(ifname string, addr *net.UDPAddr, handler Handler, opt ...ServerOpt) (*Server, error) { @@ -125,19 +130,21 @@ func NewServer(ifname string, addr *net.UDPAddr, handler Handler, opt ...ServerO for _, o := range opt { o(s) } - if s.conn != nil { return s, nil } - var err error - // no connection provided by the user, create a new one - s.conn, err = net.ListenUDP("udp6", addr) - if err != nil { - return nil, err + if addr == nil { + addr = &net.UDPAddr{ + IP: net.IPv6unspecified, + Port: dhcpv6.DefaultServerPort, + } } - var iface *net.Interface + var ( + err error + iface *net.Interface + ) if ifname == "" { iface = nil } else { @@ -146,6 +153,11 @@ func NewServer(ifname string, addr *net.UDPAddr, handler Handler, opt ...ServerO return nil, err } } + // no connection provided by the user, create a new one + s.conn, err = NewIPv6UDPConn(ifname, addr) + if err != nil { + return nil, err + } p := ipv6.NewPacketConn(s.conn) if addr.IP.IsMulticast() { diff --git a/dhcpv6/server6/server_test.go b/dhcpv6/server6/server_test.go index cf5fbea2..09cde4c5 100644 --- a/dhcpv6/server6/server_test.go +++ b/dhcpv6/server6/server_test.go @@ -33,7 +33,7 @@ func setUpClientAndServer(handler Handler) (*nclient6.Client, *Server) { IP: net.ParseIP("::1"), Port: 0, } - s, err := NewServer("lo", laddr, handler) + s, err := NewServer("", laddr, handler) if err != nil { panic(err) }