diff --git a/AGHTechDoc.md b/AGHTechDoc.md index 99bc6a8c..b0b6ca80 100644 --- a/AGHTechDoc.md +++ b/AGHTechDoc.md @@ -25,12 +25,13 @@ Contents: * API: Find clients by IP * DHCP server * DHCP server in DNS - * "Show DHCP interfaces" command - * "Show DHCP status" command - * "Check DHCP" command - * "Enable DHCP" command + * DHCP Custom Options + * API: Show DHCP interfaces + * API: Show DHCP status + * API: Check DHCP + * API: Enable DHCP * Static IP check/set - * Add a static lease + * API: Add a static lease * API: Reset DHCP configuration * DNS general settings * API: Get DNS general settings @@ -429,7 +430,20 @@ DHCP leases are used in several ways by DNS module. > PTR 100.1.168.192.in-addr.arpa. = bills-notebook. -### "Show DHCP interfaces" command +### DHCP Custom Options + +Option with arbitrary hexadecimal data: + + DEC_CODE hex HEX_DATA + +where DEC_CODE is a decimal DHCPv4 option code in range [1..255] + +Option with IP data (only 1 IP is supported): + + DEC_CODE ip IP_ADDR + + +### API: Show DHCP interfaces Request: @@ -452,7 +466,7 @@ Response: } -### "Show DHCP status" command +### API: Show DHCP status Request: @@ -487,7 +501,7 @@ Response: } -### "Check DHCP" command +### API: Check DHCP Request: @@ -535,7 +549,7 @@ If `static_ip.static` is: In order to use DHCP server a static IP address must be set. We failed to determine if this network interface is configured using static IP address. Please set a static IP address manually. -### "Enable DHCP" command +### API: Enable DHCP Request: @@ -662,7 +676,7 @@ or: systemctl restart system-networkd -### Add a static lease +### API: Add a static lease Request: diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go index f6e19d5a..46beebca 100644 --- a/dhcpd/dhcpd.go +++ b/dhcpd/dhcpd.go @@ -1,11 +1,15 @@ package dhcpd import ( + "encoding/hex" "net" "net/http" "path/filepath" + "strconv" + "strings" "time" + "github.com/AdguardTeam/AdGuardHome/util" "github.com/AdguardTeam/golibs/log" ) @@ -214,3 +218,43 @@ func (s *Server) FindMACbyIP(ip net.IP) net.HardwareAddr { func (s *Server) AddStaticLease(lease Lease) error { return s.srv4.AddStaticLease(lease) } + +// Parse option string +// Format: +// CODE TYPE VALUE +func parseOptionString(s string) (uint8, []byte) { + s = strings.TrimSpace(s) + scode := util.SplitNext(&s, ' ') + t := util.SplitNext(&s, ' ') + sval := util.SplitNext(&s, ' ') + + code, err := strconv.Atoi(scode) + if err != nil || code <= 0 || code > 255 { + return 0, nil + } + + var val []byte + + switch t { + case "hex": + val, err = hex.DecodeString(sval) + if err != nil { + return 0, nil + } + + case "ip": + ip := net.ParseIP(sval) + if ip == nil { + return 0, nil + } + val = ip + if ip.To4() != nil { + val = ip.To4() + } + + default: + return 0, nil + } + + return uint8(code), val +} diff --git a/dhcpd/dhcpd_test.go b/dhcpd/dhcpd_test.go index ab31c900..9e4222cf 100644 --- a/dhcpd/dhcpd_test.go +++ b/dhcpd/dhcpd_test.go @@ -108,3 +108,25 @@ func TestNormalizeLeases(t *testing.T) { assert.True(t, bytes.Equal(leases[1].HWAddr, []byte{2, 2, 3, 4})) assert.True(t, bytes.Equal(leases[2].HWAddr, []byte{1, 2, 3, 5})) } + +func TestOptions(t *testing.T) { + code, val := parseOptionString(" 12 hex abcdef ") + assert.Equal(t, uint8(12), code) + assert.True(t, bytes.Equal([]byte{0xab, 0xcd, 0xef}, val)) + + code, _ = parseOptionString(" 12 hex abcdef1 ") + assert.Equal(t, uint8(0), code) + + code, val = parseOptionString("123 ip 1.2.3.4") + assert.Equal(t, uint8(123), code) + assert.Equal(t, "1.2.3.4", net.IP(string(val)).String()) + + code, _ = parseOptionString("256 ip 1.1.1.1") + assert.Equal(t, uint8(0), code) + code, _ = parseOptionString("-1 ip 1.1.1.1") + assert.Equal(t, uint8(0), code) + code, _ = parseOptionString("12 ip 1.1.1.1x") + assert.Equal(t, uint8(0), code) + code, _ = parseOptionString("12 x 1.1.1.1") + assert.Equal(t, uint8(0), code) +} diff --git a/dhcpd/server.go b/dhcpd/server.go index 5aea9497..1701c780 100644 --- a/dhcpd/server.go +++ b/dhcpd/server.go @@ -50,12 +50,23 @@ type V4ServerConf struct { // 0: disable ICMPTimeout uint32 `yaml:"icmp_timeout_msec"` + // Custom Options. + // + // Option with arbitrary hexadecimal data: + // DEC_CODE hex HEX_DATA + // where DEC_CODE is a decimal DHCPv4 option code in range [1..255] + // + // Option with IP data (only 1 IP is supported): + // DEC_CODE ip IP_ADDR + Options []string `yaml:"options"` + ipStart net.IP // starting IP address for dynamic leases ipEnd net.IP // ending IP address for dynamic leases leaseTime time.Duration // the time during which a dynamic lease is considered valid dnsIPAddrs []net.IP // IPv4 addresses to return to DHCP clients as DNS server addresses routerIP net.IP // value for Option Router subnetMask net.IPMask // value for Option SubnetMask + options []dhcpOption // Server calls this function when leases data changes notify func(uint32) @@ -79,3 +90,8 @@ type V6ServerConf struct { // Server calls this function when leases data changes notify func(uint32) } + +type dhcpOption struct { + code uint8 + val []byte +} diff --git a/dhcpd/v4.go b/dhcpd/v4.go index ac0f9808..fdc63cd4 100644 --- a/dhcpd/v4.go +++ b/dhcpd/v4.go @@ -475,6 +475,10 @@ func (s *v4Server) process(req *dhcpv4.DHCPv4, resp *dhcpv4.DHCPv4) int { resp.UpdateOption(dhcpv4.OptRouter(s.conf.routerIP)) resp.UpdateOption(dhcpv4.OptSubnetMask(s.conf.subnetMask)) resp.UpdateOption(dhcpv4.OptDNS(s.conf.dnsIPAddrs...)) + + for _, opt := range s.conf.options { + resp.Options[opt.code] = opt.val + } return 1 } @@ -619,5 +623,19 @@ func v4Create(conf V4ServerConf) (DHCPServer, error) { s.conf.leaseTime = time.Second * time.Duration(conf.LeaseDuration) } + for _, o := range conf.Options { + code, val := parseOptionString(o) + if code == 0 { + log.Debug("DHCPv4: bad option string: %s", o) + continue + } + + opt := dhcpOption{ + code: code, + val: val, + } + s.conf.options = append(s.conf.options, opt) + } + return s, nil } diff --git a/dhcpd/v4_test.go b/dhcpd/v4_test.go index 208dd8fb..fe3ac2dd 100644 --- a/dhcpd/v4_test.go +++ b/dhcpd/v4_test.go @@ -178,6 +178,10 @@ func TestV4DynamicLeaseGet(t *testing.T) { GatewayIP: "192.168.10.1", SubnetMask: "255.255.255.0", notify: notify4, + Options: []string{ + "81 hex 303132", + "82 ip 1.2.3.4", + }, } sIface, err := v4Create(conf) s := sIface.(*v4Server) @@ -198,6 +202,8 @@ func TestV4DynamicLeaseGet(t *testing.T) { assert.Equal(t, "192.168.10.1", resp.ServerIdentifier().String()) assert.Equal(t, "255.255.255.0", net.IP(resp.SubnetMask()).String()) assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds()) + assert.Equal(t, []byte("012"), resp.Options[uint8(dhcpv4.OptionFQDN)]) + assert.Equal(t, "1.2.3.4", net.IP(resp.Options[uint8(dhcpv4.OptionRelayAgentInformation)]).String()) // "Request" req, _ = dhcpv4.NewRequestFromOffer(resp)