From 842181f55562ad10d5037b01a7b6147be6a7f204 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 3 May 2023 02:57:04 +0000 Subject: [PATCH] iptools: reorganizing and continuing pool work --- .gitignore | 1 + config/BUILD.bazel | 1 + config/config.go | 36 +++++------------ dhcp/options.go | 21 +++++++--- iptools/BUILD.bazel | 17 ++++++++ iptools/hardware_address.go | 41 +++++++++++++++++++ iptools/hardware_address_test.go | 31 ++++++++++++++ iptools/lease_info.go | 69 ++++++++++++++++++++++++++++++++ iptools/pool_test.go | 37 +++++++++++++++++ iptools/range.go | 61 ++++++++++++++++++++++++++++ iptools/range_test.go | 49 +++++++++++++++++++++++ iptools/tools.go | 25 ++++++++++++ server/BUILD.bazel | 1 + server/server.go | 1 + 14 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 iptools/BUILD.bazel create mode 100644 iptools/hardware_address.go create mode 100644 iptools/hardware_address_test.go create mode 100644 iptools/lease_info.go create mode 100644 iptools/pool_test.go create mode 100644 iptools/range.go create mode 100644 iptools/range_test.go create mode 100644 iptools/tools.go diff --git a/.gitignore b/.gitignore index 4e50810..7b8ec17 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ bazel-testlogs kdhcpd cmd/kdhcpd/kdhcpd +strace.txt diff --git a/config/BUILD.bazel b/config/BUILD.bazel index d66b6eb..3b6981b 100644 --- a/config/BUILD.bazel +++ b/config/BUILD.bazel @@ -9,6 +9,7 @@ go_library( importpath = "git.wntrmute.dev/kyle/kdhcp/config", visibility = ["//visibility:public"], deps = [ + "//iptools", "//log", "@in_gopkg_yaml_v2//:yaml_v2", ], diff --git a/config/config.go b/config/config.go index 13f35ee..c7f9b42 100644 --- a/config/config.go +++ b/config/config.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net" + "git.wntrmute.dev/kyle/kdhcp/iptools" "git.wntrmute.dev/kyle/kdhcp/log" "gopkg.in/yaml.v2" ) @@ -23,23 +24,6 @@ func ensureV4(ip net.IP) (net.IP, error) { return ip4, nil } -type IPRange struct { - Start net.IP - End net.IP -} - -func (r *IPRange) ensureV4() (err error) { - if r.Start, err = ensureV4(r.Start); err != nil { - return fmt.Errorf("config: range start address %w", err) - } - - if r.End, err = ensureV4(r.End); err != nil { - return fmt.Errorf("config: range end address %w", err) - } - - return nil -} - type Network struct { IP net.IP `yaml:"address"` Gateway net.IP `yaml:"gateway"` @@ -85,14 +69,14 @@ type ConfigFile struct { } type Config struct { - Version int `yaml:"version"` - Interface string `yaml:"interface"` - Address string `yaml:"address"` - Port int `yaml:"port"` - LeaseFile string `yaml:"lease_file"` - Network *Network `yaml:"network"` - Pools map[string]*IPRange `yaml:"pools"` - Statics map[string]net.IP `yaml:"statics"` + Version int `yaml:"version"` + Interface string `yaml:"interface"` + Address string `yaml:"address"` + Port int `yaml:"port"` + LeaseFile string `yaml:"lease_file"` + Network *Network `yaml:"network"` + Pools map[string]*iptools.Range `yaml:"pools"` + Statics map[string]net.IP `yaml:"statics"` } func (cfg *Config) process() (err error) { @@ -114,7 +98,7 @@ func (cfg *Config) process() (err error) { } for k, v := range cfg.Pools { - if err = v.ensureV4(); err != nil { + if err = v.Validate(); err != nil { return fmt.Errorf("config: pool %s %w", k, err) } diff --git a/dhcp/options.go b/dhcp/options.go index 7c8b8dd..288f14e 100644 --- a/dhcp/options.go +++ b/dhcp/options.go @@ -2,6 +2,7 @@ package dhcp import ( "errors" + "fmt" "io" ) @@ -11,14 +12,14 @@ type Option func(req *BootRequest, r io.Reader) error const ( OptionTagPadding OptionTag = 0 - OptionTagHostName = 12 - OptionTagMessageType = 53 - OptionTagParameterRequestList = 55 - OptionTagEnd = 255 + OptionTagHostName OptionTag = 12 + OptionTagMessageType OptionTag = 53 + OptionTagParameterRequestList OptionTag = 55 + OptionTagEnd OptionTag = 255 ) var optionRegistry = map[OptionTag]Option{ - OptionTagPadding: OptionTag, + OptionTagPadding: OptionPad, OptionTagHostName: OptionHostName, OptionTagMessageType: OptionMessageType, OptionTagParameterRequestList: OptionParameterRequestList, @@ -44,3 +45,13 @@ func OptionParameterRequestList(req *BootRequest, r io.Reader) error { func OptionEnd(req *BootRequest, r io.Reader) error { return errors.New("dhcp: option not implemented yet") } + +func ReadOption(req *BootRequest, tag byte, r io.Reader) error { + opt := OptionTag(tag) + if f, ok := optionRegistry[opt]; ok { + return f(req, r) + + } + + return fmt.Errorf("dhcp: unknown/unhandled option %d", opt) +} diff --git a/iptools/BUILD.bazel b/iptools/BUILD.bazel new file mode 100644 index 0000000..4257ee2 --- /dev/null +++ b/iptools/BUILD.bazel @@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "iptools", + srcs = [ + "pool.go", + "range.go", + ], + importpath = "git.wntrmute.dev/kyle/kdhcp/iptools", + visibility = ["//visibility:public"], +) + +go_test( + name = "iptools_test", + srcs = ["range_test.go"], + embed = [":iptools"], +) diff --git a/iptools/hardware_address.go b/iptools/hardware_address.go new file mode 100644 index 0000000..f36a542 --- /dev/null +++ b/iptools/hardware_address.go @@ -0,0 +1,41 @@ +package iptools + +import ( + "bytes" + "fmt" + "strconv" + "strings" +) + +type HardwareAddress []byte + +func (mac HardwareAddress) String() string { + marshalled := []string{} + for i := 0; i < len(mac); i++ { + marshalled = append(marshalled, fmt.Sprintf("%02x", []byte(mac[i:i+1]))) + } + + return strings.Join(marshalled, ":") +} + +func (mac HardwareAddress) MarshalText() ([]byte, error) { + return []byte(mac.String()), nil +} + +func (mac *HardwareAddress) UnmarshalText(b []byte) error { + rb := bytes.Split(b, []byte(":")) + for _, octet := range rb { + n, err := strconv.ParseUint(string(octet), 16, 8) + if err != nil { + return err + } + + *mac = append(*mac, uint8(n)) + } + + return nil +} + +func (mac HardwareAddress) Match(other HardwareAddress) bool { + return bytes.Equal(mac, other) +} diff --git a/iptools/hardware_address_test.go b/iptools/hardware_address_test.go new file mode 100644 index 0000000..9d8f273 --- /dev/null +++ b/iptools/hardware_address_test.go @@ -0,0 +1,31 @@ +package iptools + +import ( + "bytes" + "testing" +) + +func TestHardwareMacMarshalling(t *testing.T) { + macString := "b8:27:eb:b6:a1:a7" + mac := HardwareAddress([]byte{0xb8, 0x27, 0xeb, 0xb6, 0xa1, 0xa7}) + + b, err := mac.MarshalText() + if err != nil { + t.Fatal(err) + } + + s := string(b) + if s != macString { + t.Fatalf("have %s, want %s", s, macString) + } + + mac2 := &HardwareAddress{} + err = mac2.UnmarshalText(b) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(*mac2, mac) { + t.Fatalf("have %x, want %x", *mac2, mac) + } +} diff --git a/iptools/lease_info.go b/iptools/lease_info.go new file mode 100644 index 0000000..fe4e725 --- /dev/null +++ b/iptools/lease_info.go @@ -0,0 +1,69 @@ +package iptools + +import ( + "fmt" + "net/netip" + "sort" + "strings" + "time" +) + +type LeaseInfo struct { + HostName string `yaml:"hostname"` + Addr netip.Addr `yaml:"addr"` + HardwareAddress HardwareAddress `yaml:"mac_addr"` + Expires time.Time `yaml:"expires"` +} + +type sortableLease []LeaseInfo + +func (a sortableLease) Len() int { return len(a) } +func (a sortableLease) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortableLease) Less(i, j int) bool { return a[i].Addr.Less(a[j].Addr) } + +func (li *LeaseInfo) ResetExpiry(t time.Time, dur time.Duration) { + li.Expires = t.Add(dur) +} + +func (li *LeaseInfo) IsExpired(t time.Time) bool { + return t.After(li.Expires) +} + +func (li *LeaseInfo) Expire() { + li.Expires = time.Time{} +} + +func SortLeases(leases []LeaseInfo) []LeaseInfo { + sortable := sortableLease(leases) + sort.Sort(sortable) + + return []LeaseInfo(sortable) +} + +func (lease LeaseInfo) Reset() LeaseInfo { + lease.Expires = time.Time{} + lease.HardwareAddress = nil + return lease +} + +func (lease LeaseInfo) Compare(other LeaseInfo) error { + susFields := []string{} + + if lease.Addr != other.Addr { + susFields = append(susFields, fmt.Sprintf("address is %s, but is recorded as %s", lease.Addr, other.Addr)) + } + + if !lease.HardwareAddress.Match(other.HardwareAddress) { + susFields = append(susFields, fmt.Sprintf("hardware address is %s, but is recorded as %s", lease.HardwareAddress, other.HardwareAddress)) + } + + if lease.HostName != other.HostName { + susFields = append(susFields, fmt.Sprintf("hostname is %s, but is recorded as %s", lease.HostName, other.HostName)) + } + + if len(susFields) > 0 { + return fmt.Errorf("suspicious lease: %s", strings.Join(susFields, ";")) + } + + return nil +} diff --git a/iptools/pool_test.go b/iptools/pool_test.go new file mode 100644 index 0000000..7aa9d77 --- /dev/null +++ b/iptools/pool_test.go @@ -0,0 +1,37 @@ +package iptools + +import ( + "fmt" + "net/netip" + "testing" + "time" +) + +var ( + poolTestIP1 = netip.MustParseAddr("192.168.4.1") + poolTestIP2 = netip.MustParseAddr("192.168.4.32") +) + +func TestBasicPool(t *testing.T) { + r := &Range{ + Start: poolTestIP1, + End: poolTestIP2, + } + + p, err := NewPool("cluster", 24*time.Hour, r) + if err != nil { + t.Fatal(err) + } + + if len(p.Available) != 32 { + t.Fatalf("have %d available leases, want %d", len(p.Available), 32) + } + + for i := range p.Available { + l := p.Available[i] + expectedName := fmt.Sprintf("cluster%02d", l.Addr.As4()[3]) + if l.HostName != expectedName { + t.Fatalf("have hostname %s, want %s", l.HostName, expectedName) + } + } +} diff --git a/iptools/range.go b/iptools/range.go new file mode 100644 index 0000000..98978fa --- /dev/null +++ b/iptools/range.go @@ -0,0 +1,61 @@ +package iptools + +import ( + "fmt" + "net/netip" +) + +const ( + DefaultMaskBits = 24 +) + +type Range struct { + Start netip.Addr `yaml:"start"` + End netip.Addr `yaml:"end"` + Network netip.Prefix `yaml:"network"` +} + +func (r *Range) Validate() error { + if !r.Start.Is4() { + return fmt.Errorf("range start %s is not a valid IPv4 address", r.Start) + } + + if !r.End.Is4() { + return fmt.Errorf("range end %s is not a valid IPv4 address", r.End) + } + + // Compare returns -1 if lhs < rhs, 0 if lhs == rhs, and 1 if lhs > rhs. + if r.End.Compare(r.Start) != 1 { + return fmt.Errorf("start address %s is not before end address %s", r.Start, r.End) + } + + var err error + if !r.Network.IsValid() { + r.Network, err = r.Start.Prefix(DefaultMaskBits) + if err != nil { + return err + } + } + + if !r.Network.Contains(r.Start) { + return fmt.Errorf("prefix %s does not contain start address %s", r.Network, r.Start) + } + + if !r.Network.Contains(r.End) { + return fmt.Errorf("prefix %s does not contain end address %s", r.Network, r.End) + } + + return nil +} + +// this is probably dumb, but it's a one-time cost upfront on pool instantiation. +func (r *Range) numHosts() int { + cur := r.Start + hosts := 0 + for cur.Compare(r.End) < 1 { + hosts++ + cur.Next() + } + + return hosts +} diff --git a/iptools/range_test.go b/iptools/range_test.go new file mode 100644 index 0000000..a62f146 --- /dev/null +++ b/iptools/range_test.go @@ -0,0 +1,49 @@ +package iptools + +import ( + "net/netip" + "testing" +) + +var ( + rangeTestIP1 = netip.AddrFrom4([4]byte{192, 168, 4, 3}) + rangeTestIP2 = netip.AddrFrom4([4]byte{192, 168, 4, 17}) +) + +func TestBasicValidation(t *testing.T) { + r1 := &Range{ + Start: rangeTestIP1, + End: rangeTestIP2, + } + + if err := r1.Validate(); err != nil { + t.Fatalf("range 1 should be valid: %s", err) + } + + r2 := &Range{ + Start: rangeTestIP2, + End: rangeTestIP1, + } + + if r2.Validate() == nil { + t.Fatal("range 2 should be invalid") + } + + r3 := &Range{ + Start: netip.IPv6LinkLocalAllRouters(), + End: rangeTestIP1, + } + + if r3.Validate() == nil { + t.Fatal("range 3 should be invalid") + } + + r4 := &Range{ + Start: rangeTestIP2, + End: netip.IPv6LinkLocalAllRouters(), + } + + if r4.Validate() == nil { + t.Fatal("range 4 should be invalid") + } +} diff --git a/iptools/tools.go b/iptools/tools.go new file mode 100644 index 0000000..5056a7c --- /dev/null +++ b/iptools/tools.go @@ -0,0 +1,25 @@ +package iptools + +import "fmt" + +func enumerateRange(name string, r *Range, startFromOne bool) []LeaseInfo { + start := r.Start + cur := start + lenfmt := fmt.Sprintf("%%s%%0%dd", len(fmt.Sprintf("%d", r.numHosts()))) + i := 0 + if startFromOne { + i++ + } + leases := []LeaseInfo{} + + for r.End.Compare(cur) >= 0 { + leases = append(leases, LeaseInfo{ + HostName: fmt.Sprintf(lenfmt, name, i), + Addr: cur, + }) + i++ + cur = cur.Next() + } + + return leases +} diff --git a/server/BUILD.bazel b/server/BUILD.bazel index cefe30d..89ebadc 100644 --- a/server/BUILD.bazel +++ b/server/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "server", srcs = [ "ifi.go", + "ifi_linux.go", "server.go", ], importpath = "git.wntrmute.dev/kyle/kdhcp/server", diff --git a/server/server.go b/server/server.go index 94960cc..784f296 100644 --- a/server/server.go +++ b/server/server.go @@ -70,6 +70,7 @@ func (s *Server) Listen() { log.Errf("server: error reading packet: %s", err) continue } + break } }