dhcpd: fixed a bug in discovery

This commit is contained in:
nadoo 2022-02-24 18:41:03 +08:00
parent c261e5989c
commit 9f6e5ebb98
14 changed files with 188 additions and 129 deletions

View File

@ -96,7 +96,7 @@ glider -verbose -listen :8443 -forward SCHEME://HOST:PORT
#### Help
<details>
<summary>`glider -help` (click)</summary>
<summary>glider -help</summary>
```bash
Usage: glider [-listen URL]... [-forward URL]... [OPTION]...
@ -228,7 +228,7 @@ glider 0.16.0, https://github.com/nadoo/glider
#### Schemes
<details>
<summary>`glider -scheme all` (click)</summary>
<summary>glider -scheme all</summary>
```bash
KCP scheme:
@ -343,7 +343,7 @@ TLS and Websocket with a specified proxy protocol:
#### Examples
<details>
<summary>`glider -example` (click)</summary>
<summary>glider -example</summary>
```bash
Examples:
@ -396,9 +396,8 @@ Examples:
- service=dhcpd,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
- service=dhcpd,eth1,192.168.1.100,192.168.1.199,720
- service=dhcpd,eth2,192.168.2.100,192.168.2.199,720,fc:23:34:9e:25:01=192.168.2.101
- dhcpd-failover: only serves requests when there's no other dhcp server exists
- service=dhcpd-failover,INTERFACE,START_IP,END_IP,LEASE_MINUTES[,MAC=IP,MAC=IP...]
- note: only serve requests when there's no other dhcp server exists
## Linux Service

2
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/nadoo/ipset v0.4.1-0.20220218075046-ca3cdce74266
github.com/xtaci/kcp-go/v5 v5.6.1
golang.org/x/crypto v0.0.0-20220214200702-86341886e292
golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c
golang.org/x/sys v0.0.0-20220224003255-dbe011f71a99
)
require (

5
go.sum
View File

@ -49,6 +49,7 @@ github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqo
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/klauspost/cpuid v1.2.4/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s=
github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.11 h1:i2lw1Pm7Yi/4O6XCSyJWqEHI2MDw2FzUK6o/D21xn2A=
@ -164,8 +165,8 @@ golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c h1:sSIdNI2Dd6vGv47bKc/xArpfxVmEz2+3j0E6I484xC4=
golang.org/x/sys v0.0.0-20220222200937-f2425489ef4c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220224003255-dbe011f71a99 h1:Us899Z5PCfOrSgeCYWobI1/bSigAz9Rhf8+fz5Grkzc=
golang.org/x/sys v0.0.0-20220224003255-dbe011f71a99/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -5,7 +5,6 @@ import (
"net"
"os"
"os/signal"
"strings"
"syscall"
"time"
@ -81,8 +80,11 @@ func main() {
// run services
for _, s := range config.Services {
args := strings.Split(s, ",")
go service.Run(args[0], args[1:]...)
service, err := service.New(s)
if err != nil {
log.Fatal(err)
}
go service.Run()
}
sigCh := make(chan os.Signal, 1)

View File

@ -1,21 +0,0 @@
package sockopt
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
func BindControl(iface *net.Interface) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) (err error) {
return c.Control(func(fd uintptr) {
switch network {
case "tcp4", "udp4":
err = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, iface.Index)
case "tcp6", "udp6":
err = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, iface.Index)
}
})
}
}

View File

@ -1,16 +0,0 @@
package sockopt
import (
"net"
"syscall"
"golang.org/x/sys/unix"
)
func BindControl(iface *net.Interface) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) (err error) {
return c.Control(func(fd uintptr) {
err = unix.BindToDevice(int(fd), iface.Name)
})
}
}

31
pkg/sockopt/sockopt.go Normal file
View File

@ -0,0 +1,31 @@
package sockopt
import (
"net"
"syscall"
)
// Options is the options struct.
type Options struct {
bindIface *net.Interface
reuseAddr bool
}
// Option is the function paramater.
type Option func(opts *Options)
// Bind sets the bind interface option.
func Bind(intf *net.Interface) Option { return func(opts *Options) { opts.bindIface = intf } }
// ReuseAddr sets the reuse addr option.
func ReuseAddr() Option { return func(opts *Options) { opts.reuseAddr = true } }
// Control returns a control function for the net.Dialer and net.ListenConfig.
func Control(opts ...Option) func(network, address string, c syscall.RawConn) error {
option := &Options{}
for _, opt := range opts {
opt(option)
}
return control(option)
}

View File

@ -0,0 +1,28 @@
package sockopt
import (
"syscall"
"golang.org/x/sys/unix"
)
func control(opt *Options) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) (err error) {
return c.Control(func(fd uintptr) {
if opt.bindIface != nil {
switch network {
case "tcp4", "udp4":
unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, opt.bindIface.Index)
case "tcp6", "udp6":
unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, opt.bindIface.Index)
}
}
if opt.reuseAddr {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}
})
}
}

View File

@ -0,0 +1,23 @@
package sockopt
import (
"syscall"
"golang.org/x/sys/unix"
)
func control(opt *Options) func(network, address string, c syscall.RawConn) error {
return func(network, address string, c syscall.RawConn) (err error) {
return c.Control(func(fd uintptr) {
if opt.bindIface != nil {
unix.BindToDevice(int(fd), opt.bindIface.Name)
}
if opt.reuseAddr {
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
}
})
}
}

View File

@ -8,4 +8,4 @@ import (
"syscall"
)
func BindControl(iface *net.Interface) func(string, string, syscall.RawConn) error { return nil }
func control(opt *Options) func(string, string, syscall.RawConn) error { return nil }

View File

@ -89,7 +89,7 @@ func (d *Direct) dial(network, addr string, localIP net.IP) (net.Conn, error) {
dialer := &net.Dialer{LocalAddr: la, Timeout: d.dialTimeout}
if d.iface != nil {
dialer.Control = sockopt.BindControl(d.iface)
dialer.Control = sockopt.Control(sockopt.Bind(d.iface))
}
c, err := dialer.Dial(network, addr)
@ -117,7 +117,7 @@ func (d *Direct) DialUDP(network, addr string) (net.PacketConn, net.Addr, error)
lc := &net.ListenConfig{}
if d.iface != nil {
lc.Control = sockopt.BindControl(d.iface)
lc.Control = sockopt.Control(sockopt.Bind(d.iface))
}
pc, err := lc.ListenPacket(context.Background(), network, la)

View File

@ -11,16 +11,15 @@ import (
)
func discovery(intf *net.Interface) (found bool) {
lc := &net.ListenConfig{}
lc.Control = sockopt.BindControl(intf)
lc := &net.ListenConfig{Control: sockopt.Control(sockopt.Bind(intf), sockopt.ReuseAddr())}
pc, err := lc.ListenPacket(context.Background(), "udp4", "255.255.255.255:68")
pc, err := lc.ListenPacket(context.Background(), "udp4", ":68")
if err != nil {
return
}
defer pc.Close()
discovery, err := dhcpv4.NewDiscovery(intf.HardwareAddr, dhcpv4.WithBroadcast(true))
discovery, err := dhcpv4.NewDiscovery(intf.HardwareAddr, dhcpv4.WithBroadcast(true), dhcpv4.WithRequestedOptions(dhcpv4.OptionDomainNameServer))
if err != nil {
return
}
@ -37,10 +36,6 @@ func discovery(intf *net.Interface) (found bool) {
return
}
msg, err := dhcpv4.FromBytes(buf[:n])
if err != nil || msg.TransactionID != discovery.TransactionID {
return
}
return true
_, err = dhcpv4.FromBytes(buf[:n])
return err == nil
}

View File

@ -3,6 +3,7 @@ package dhcpd
import (
"bytes"
"errors"
"fmt"
"net"
"net/netip"
"strconv"
@ -17,60 +18,60 @@ import (
"github.com/nadoo/glider/service"
)
var leaseTime = time.Hour * 12
func init() {
service.Register("dhcpd", &dhcpd{})
service.Register("dhcpd-failover", &dhcpd{detect: true})
service.Register("dhcpd", NewService)
service.Register("dhcpd-failover", NewFailOverService)
}
type dhcpd struct {
mu sync.Mutex
detect bool
failover bool
intface *net.Interface
name string
pool *Pool
lease time.Duration
iface *net.Interface
server *server4.Server
}
// Run runs the service.
func (d *dhcpd) Run(args ...string) {
// NewService returns a new dhcpd Service.
func NewService(args ...string) (service.Service, error) { return New(false, args...) }
// NewService returns a new dhcpd Service with failover mode on.
func NewFailOverService(args ...string) (service.Service, error) { return New(true, args...) }
// New returns a new dhcpd instance.
func New(failover bool, args ...string) (*dhcpd, error) {
if len(args) < 4 {
log.F("[dhcpd] not enough parameters, exiting")
return
return nil, errors.New("not enough parameters, exiting")
}
iface, start, end, leaseMin := args[0], args[1], args[2], args[3]
if i, err := strconv.Atoi(leaseMin); err != nil {
leaseTime = time.Duration(i) * time.Minute
}
intf, ip, mask, err := ifaceAddr(iface)
if err != nil {
log.F("[dhcpd] get ip of interface '%s' error: %s", iface, err)
return
}
d.intface = intf
if d.detect {
d.setFailoverMode(discovery(intf))
go d.detectServer(time.Second * 60)
return nil, fmt.Errorf("get ip of interface '%s' error: %s", iface, err)
}
startIP, err := netip.ParseAddr(start)
if err != nil {
log.F("[dhcpd] startIP %s is not valid: %s", start, err)
return
return nil, fmt.Errorf("startIP %s is not valid: %s", start, err)
}
endIP, err := netip.ParseAddr(end)
if err != nil {
log.F("[dhcpd] endIP %s is not valid: %s", end, err)
return
return nil, fmt.Errorf("endIP %s is not valid: %s", end, err)
}
pool, err := NewPool(leaseTime, startIP, endIP)
var lease = time.Hour * 12
if i, err := strconv.Atoi(leaseMin); err == nil {
lease = time.Duration(i) * time.Minute
} else {
return nil, fmt.Errorf("LEASE_MINUTES %s is not valid: %s", end, err)
}
pool, err := NewPool(lease, startIP, endIP)
if err != nil {
log.F("[dhcpd] error in pool init: %s", err)
return
return nil, fmt.Errorf("error in pool init: %s", err)
}
// static ips
@ -84,17 +85,33 @@ func (d *dhcpd) Run(args ...string) {
}
}
laddr := net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 67}
server, err := server4.NewServer(iface, &laddr, d.handleDHCP(ip, mask, pool))
if err != nil {
log.F("[dhcpd] error in server creation: %s", err)
return
dhcpd := &dhcpd{
name: intf.Name,
iface: intf,
pool: pool,
lease: lease,
failover: failover,
}
log.F("[dhcpd] Listening on interface %s(%s/%d.%d.%d.%d), server detection: %t",
iface, ip, mask[0], mask[1], mask[2], mask[3], d.detect)
if dhcpd.server, err = server4.NewServer(
iface, &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: 67},
dhcpd.handleDHCP(ip, mask, pool)); err != nil {
return nil, fmt.Errorf("error in server creation: %s", err)
}
server.Serve()
log.F("[dhcpd] Listening on interface %s(%s/%d.%d.%d.%d), failover mode: %t",
iface, ip, mask[0], mask[1], mask[2], mask[3], dhcpd.getFailover())
return dhcpd, nil
}
// Run runs the service.
func (d *dhcpd) Run() {
if d.failover {
d.setFailover(discovery(d.iface))
go d.detect(time.Second * 60)
}
d.server.Serve()
}
func (d *dhcpd) handleDHCP(serverIP net.IP, mask net.IPMask, pool *Pool) server4.Handler {
@ -108,24 +125,24 @@ func (d *dhcpd) handleDHCP(serverIP net.IP, mask net.IPMask, pool *Pool) server4
replyType = dhcpv4.MessageTypeAck
case dhcpv4.MessageTypeRelease:
pool.ReleaseIP(m.ClientHWAddr)
log.F("[dpcpd] %v released ip %v", m.ClientHWAddr, m.ClientIPAddr)
log.F("[dpcpd] %s:%v released ip %v", d.name, m.ClientHWAddr, m.ClientIPAddr)
return
case dhcpv4.MessageTypeDecline:
pool.ReleaseIP(m.ClientHWAddr)
log.F("[dpcpd] received decline message from %v", m.ClientHWAddr)
log.F("[dpcpd] %s: received decline message from %v", d.name, m.ClientHWAddr)
return
default:
log.F("[dpcpd] can't handle type %v", reqType)
log.F("[dpcpd] %s: can't handle type %v", d.name, reqType)
return
}
if d.inFailoverMode() || bytes.Equal(d.intface.HardwareAddr, m.ClientHWAddr) {
if d.getFailover() || bytes.Equal(d.iface.HardwareAddr, m.ClientHWAddr) {
return
}
replyIP, err := pool.LeaseIP(m.ClientHWAddr)
if err != nil {
log.F("[dpcpd] can not assign IP, error %s", err)
log.F("[dpcpd] %s: can not assign IP, error %s", d.name, err)
return
}
@ -139,10 +156,10 @@ func (d *dhcpd) handleDHCP(serverIP net.IP, mask net.IPMask, pool *Pool) server4
// RFC 2131, Section 4.3.1. Server Identifier: MUST
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(serverIP)),
// RFC 2131, Section 4.3.1. IP lease time: MUST
dhcpv4.WithOption(dhcpv4.OptIPAddressLeaseTime(leaseTime)),
dhcpv4.WithOption(dhcpv4.OptIPAddressLeaseTime(d.lease)),
)
if err != nil {
log.F("[dpcpd] can not create reply message, error %s", err)
log.F("[dpcpd] %s: can not create reply message, error %s", d.name, err)
return
}
@ -151,37 +168,37 @@ func (d *dhcpd) handleDHCP(serverIP net.IP, mask net.IPMask, pool *Pool) server4
}
if _, err := conn.WriteTo(reply.ToBytes(), peer); err != nil {
log.F("[dpcpd] could not write to client %s(%s): %s", peer, reply.ClientHWAddr, err)
log.F("[dpcpd] %s: could not write to client %s(%s): %s", d.name, peer, reply.ClientHWAddr, err)
return
}
log.F("[dpcpd] lease %v to client %v", replyIP, reply.ClientHWAddr)
log.F("[dpcpd] %s: lease %v to client %v", d.name, replyIP, reply.ClientHWAddr)
}
}
func (d *dhcpd) inFailoverMode() bool {
func (d *dhcpd) getFailover() bool {
d.mu.Lock()
defer d.mu.Unlock()
return d.failover
}
func (d *dhcpd) setFailoverMode(v bool) {
func (d *dhcpd) setFailover(v bool) {
d.mu.Lock()
defer d.mu.Unlock()
if d.failover != v {
if v {
log.F("[dpcpd] existing dhcp server detected, enter failover mode")
log.F("[dpcpd] %s: dhcp server detected, enter failover mode", d.iface.Name)
} else {
log.F("[dpcpd] no dhcp server detected, exit failover mode")
log.F("[dpcpd] %s: no dhcp server detected, exit failover mode", d.iface.Name)
}
}
d.failover = v
}
func (d *dhcpd) detectServer(interval time.Duration) {
func (d *dhcpd) detect(interval time.Duration) {
for {
d.setFailoverMode(discovery(d.intface))
d.setFailover(discovery(d.iface))
time.Sleep(interval)
}
}

View File

@ -1,29 +1,29 @@
package service
import (
"errors"
"strings"
"github.com/nadoo/glider/pkg/log"
)
// Service is a server that can be run.
type Service interface {
Run(args ...string)
}
var creators = make(map[string]Creator)
var services = make(map[string]Service)
// Service is a server that can be run.
type Service interface{ Run() }
// Creator is a function to create services.
type Creator func(args ...string) (Service, error)
// Register is used to register a service.
func Register(name string, s Service) {
services[strings.ToLower(name)] = s
func Register(name string, c Creator) {
creators[strings.ToLower(name)] = c
}
// Run runs a service.
func Run(name string, args ...string) {
svc, ok := services[strings.ToLower(name)]
if !ok {
log.F("[service] unknown service name: %s", name)
return
// New calls the registered creator to create services.
func New(s string) (Service, error) {
args := strings.Split(s, ",")
c, ok := creators[strings.ToLower(args[0])]
if ok {
return c(args[1:]...)
}
svc.Run(args...)
return nil, errors.New("unknown service name: '" + args[0] + "'")
}