From cc7f8b85b5d7906b5927a631ad82842550ab5858 Mon Sep 17 00:00:00 2001 From: Samuel Lorch Date: Sun, 23 Apr 2023 19:52:30 +0200 Subject: [PATCH] wip: implement dhcp v4 server --- internal/dhcp_server/apply.go | 103 ++++++++++++++++++ internal/dhcp_server/defaults.go | 43 ++++++++ internal/dhcp_server/dhcpv4.go | 17 +++ internal/dhcp_server/dhcpv6.go | 17 +++ internal/dhcp_server/template.go | 107 +++++++++++++++++++ internal/dhcp_server/template/default.tmpl | 2 + internal/dhcp_server/template/v4_config.tmpl | 46 ++++++++ internal/dhcp_server/template/v6_config.tmpl | 0 main.go | 8 +- 9 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 internal/dhcp_server/apply.go create mode 100644 internal/dhcp_server/defaults.go create mode 100644 internal/dhcp_server/dhcpv4.go create mode 100644 internal/dhcp_server/dhcpv6.go create mode 100644 internal/dhcp_server/template.go create mode 100644 internal/dhcp_server/template/default.tmpl create mode 100644 internal/dhcp_server/template/v4_config.tmpl create mode 100644 internal/dhcp_server/template/v6_config.tmpl diff --git a/internal/dhcp_server/apply.go b/internal/dhcp_server/apply.go new file mode 100644 index 0000000..a1bd6e3 --- /dev/null +++ b/internal/dhcp_server/apply.go @@ -0,0 +1,103 @@ +package dhcp + +import ( + "context" + "fmt" + "os" + + systemctl "github.com/coreos/go-systemd/v22/dbus" + "nfsense.net/nfsense/internal/definitions/config" +) + +const defaultsFile = "/etc/default/isc-dhcp-server" +const dhcpv4File = "/etc/dhcp/dhcpd.conf" +const dhcpv6File = "/etc/dhcp/dhcpd6.conf" + +func ApplyDHCPServerConfiguration(currentConfig config.Config, pendingConfig config.Config) error { + defaultsConfig, err := GenerateDHCPServerDefaultsConfiguration(pendingConfig) + if err != nil { + return fmt.Errorf("Generating DHCPServer Defaults Configuration: %w", err) + } + + v4Conf, err := GenerateDHCPServerV4Configuration(pendingConfig) + if err != nil { + return fmt.Errorf("Generating DHCPServerV4 Configuration: %w", err) + } + + v6Conf, err := GenerateDHCPServerV6Configuration(pendingConfig) + if err != nil { + return fmt.Errorf("Generating DHCPServerV6 Configuration: %w", err) + } + + err = OverwriteFile(defaultsFile, defaultsConfig) + if err != nil { + return fmt.Errorf("Writing defaults Configuration: %w", err) + } + + err = OverwriteFile(dhcpv4File, v4Conf) + if err != nil { + return fmt.Errorf("Writing v4 Configuration: %w", err) + } + + err = OverwriteFile(dhcpv6File, v6Conf) + if err != nil { + return fmt.Errorf("Writing v6 Configuration: %w", err) + } + + conn, err := systemctl.NewSystemConnectionContext(context.Background()) + if err != nil { + return fmt.Errorf("Opening Dbus Connection: %w", err) + } + + if len(pendingConfig.Service.DHCPv4Servers) == 0 && len(pendingConfig.Service.DHCPv6Servers) == 0 { + // if there are no servers stop the service instead + _, err := conn.StopUnitContext(context.Background(), "isc-dhcp-server.service", "replace", nil) + if err != nil { + return fmt.Errorf("stopping isc-dhcp-server.service: %w", err) + } + + _, err = conn.DisableUnitFilesContext(context.Background(), []string{"isc-dhcp-server.service"}, false) + if err != nil { + return fmt.Errorf("disableing isc-dhcp-server.service: %w", err) + } + } else { + _, err := conn.ReloadOrRestartUnitContext(context.Background(), "isc-dhcp-server.service", "replace", nil) + if err != nil { + return fmt.Errorf("restarting isc-dhcp-server.service: %w", err) + } + + _, _, err = conn.EnableUnitFilesContext(context.Background(), []string{"isc-dhcp-server.service"}, false, true) + if err != nil { + return fmt.Errorf("enableing isc-dhcp-server.service: %w", err) + } + } + return nil +} + +func OverwriteFile(path, content string) error { + f, err := os.OpenFile(path, os.O_RDWR, 0644) + if err != nil { + return fmt.Errorf("opening File: %w", err) + } + + err = f.Truncate(0) + if err != nil { + return fmt.Errorf("truncate File: %w", err) + } + + _, err = f.Seek(0, 0) + if err != nil { + return fmt.Errorf("seek File: %w", err) + } + + _, err = f.WriteString(content + "\n") + if err != nil { + return fmt.Errorf("writing File: %w", err) + } + + err = f.Sync() + if err != nil { + return fmt.Errorf("syncing File: %w", err) + } + return nil +} diff --git a/internal/dhcp_server/defaults.go b/internal/dhcp_server/defaults.go new file mode 100644 index 0000000..4ee0679 --- /dev/null +++ b/internal/dhcp_server/defaults.go @@ -0,0 +1,43 @@ +package dhcp + +import ( + "bytes" + "fmt" + + "nfsense.net/nfsense/internal/definitions/config" + "nfsense.net/nfsense/internal/definitions/network" +) + +type DHCPServerInterfaces struct { + V4 []string + V6 []string +} + +func GenerateDHCPServerDefaultsConfiguration(conf config.Config) (string, error) { + v4 := []string{} + for _, s := range conf.Service.DHCPv4Servers { + if conf.Network.Interfaces[s.Interface].Type == network.Hardware { + v4 = append(v4, *conf.Network.Interfaces[s.Interface].HardwareDevice) + } else { + v4 = append(v4, s.Interface) + } + } + v6 := []string{} + for _, s := range conf.Service.DHCPv6Servers { + if conf.Network.Interfaces[s.Interface].Type == network.Hardware { + v6 = append(v6, *conf.Network.Interfaces[s.Interface].HardwareDevice) + } else { + v6 = append(v6, s.Interface) + } + } + interfaces := DHCPServerInterfaces{ + V4: v4, + V6: v6, + } + buf := new(bytes.Buffer) + err := templates.ExecuteTemplate(buf, "default.tmpl", interfaces) + if err != nil { + return "", fmt.Errorf("executing default.tmpl template: %w", err) + } + return buf.String(), nil +} diff --git a/internal/dhcp_server/dhcpv4.go b/internal/dhcp_server/dhcpv4.go new file mode 100644 index 0000000..64c1fb8 --- /dev/null +++ b/internal/dhcp_server/dhcpv4.go @@ -0,0 +1,17 @@ +package dhcp + +import ( + "bytes" + "fmt" + + "nfsense.net/nfsense/internal/definitions/config" +) + +func GenerateDHCPServerV4Configuration(conf config.Config) (string, error) { + buf := new(bytes.Buffer) + err := templates.ExecuteTemplate(buf, "v4_config.tmpl", conf) + if err != nil { + return "", fmt.Errorf("executing config.tmpl template: %w", err) + } + return buf.String(), nil +} diff --git a/internal/dhcp_server/dhcpv6.go b/internal/dhcp_server/dhcpv6.go new file mode 100644 index 0000000..1ba550c --- /dev/null +++ b/internal/dhcp_server/dhcpv6.go @@ -0,0 +1,17 @@ +package dhcp + +import ( + "bytes" + "fmt" + + "nfsense.net/nfsense/internal/definitions/config" +) + +func GenerateDHCPServerV6Configuration(conf config.Config) (string, error) { + buf := new(bytes.Buffer) + err := templates.ExecuteTemplate(buf, "v6_config.tmpl", conf) + if err != nil { + return "", fmt.Errorf("executing config.tmpl template: %w", err) + } + return buf.String(), nil +} diff --git a/internal/dhcp_server/template.go b/internal/dhcp_server/template.go new file mode 100644 index 0000000..efaa6f3 --- /dev/null +++ b/internal/dhcp_server/template.go @@ -0,0 +1,107 @@ +package dhcp + +import ( + "embed" + "fmt" + "net" + "net/netip" + "strconv" + "strings" + "text/template" + "time" + + "nfsense.net/nfsense/internal/definitions/config" + "nfsense.net/nfsense/internal/util" +) + +//go:embed template +var templateFS embed.FS +var templates *template.Template + +func init() { + var err error + templates, err = template.New("").Funcs(template.FuncMap{ + "getInterfaceAddress": getInterfaceAddress, + "getInterfaceNetworkAddress": getInterfaceNetworkAddress, + "getInterfaceBroadcastAddress": getInterfaceBroadcastAddress, + "getInterfaceNetworkMask": getInterfaceNetworkMask, + "getAddressObjectsAsCommaList": getAddressObjectsAsCommaList, + "getAddressObjectAsPoolRange": getAddressObjectAsPoolRange, + "getTimeInSecond": getTimeInSecond, + }).ParseFS(templateFS, "template/*.tmpl") + if err != nil { + panic(err) + } +} + +func getInterfaceAddress(conf config.Config, name string) string { + return conf.Network.Interfaces[name].Address.Addr().String() +} + +func getInterfaceNetworkAddress(conf config.Config, name string) string { + return conf.Network.Interfaces[name].Address.Masked().Addr().String() +} + +func getInterfaceBroadcastAddress(conf config.Config, name string) string { + return util.BroadcastAddr(prefix2IPNet(*conf.Network.Interfaces[name].Address)).String() +} + +func getInterfaceNetworkMask(conf config.Config, name string) string { + return NetMaskToString(conf.Network.Interfaces[name].Address.Bits()) +} + +func getAddressObjectsAsCommaList(conf config.Config, names []string) string { + res := "" + for i, name := range names { + res = res + conf.Object.Addresses[name].Host.String() + if len(names)-1 != i { + res = res + ", " + } + } + return res +} + +func getAddressObjectAsPoolRange(conf config.Config, name string) string { + // TODO + return strings.ReplaceAll(conf.Object.Addresses[name].Range.String(), "-", " ") +} + +func getTimeInSecond(dur time.Duration) string { + return fmt.Sprintf("%d", int(dur.Seconds())) +} + +func prefix2IPNet(prefix netip.Prefix) net.IPNet { + addr := prefix.Addr() // extract the address portion of the prefix + pLen := 128 // plen is the total size of the subnet mask + if addr.Is4() { + pLen = 32 + } + ones := prefix.Bits() // ones is the portion of the mask that's set + ip := net.IP(addr.AsSlice()) // convert the address portion to net.IP + mask := net.CIDRMask(ones, pLen) // create a net.IPMask + return net.IPNet{ // and construct the final IPNet + IP: ip, + Mask: mask, + } +} + +func NetMaskToString(mask int) string { + var binarystring string + + for ii := 1; ii <= mask; ii++ { + binarystring = binarystring + "1" + } + for ii := 1; ii <= (32 - mask); ii++ { + binarystring = binarystring + "0" + } + oct1 := binarystring[0:8] + oct2 := binarystring[8:16] + oct3 := binarystring[16:24] + oct4 := binarystring[24:] + + ii1, _ := strconv.ParseInt(oct1, 2, 64) + ii2, _ := strconv.ParseInt(oct2, 2, 64) + ii3, _ := strconv.ParseInt(oct3, 2, 64) + ii4, _ := strconv.ParseInt(oct4, 2, 64) + return strconv.Itoa(int(ii1)) + "." + strconv.Itoa(int(ii2)) + "." + strconv.Itoa(int(ii3)) + "." + strconv.Itoa(int(ii4)) +} diff --git a/internal/dhcp_server/template/default.tmpl b/internal/dhcp_server/template/default.tmpl new file mode 100644 index 0000000..d2af7b4 --- /dev/null +++ b/internal/dhcp_server/template/default.tmpl @@ -0,0 +1,2 @@ +INTERFACESv4="{{range .V4}}{{.}} {{end}}" +INTERFACESv6="{{range .V6}}{{.}} {{end}}" \ No newline at end of file diff --git a/internal/dhcp_server/template/v4_config.tmpl b/internal/dhcp_server/template/v4_config.tmpl new file mode 100644 index 0000000..33cdec4 --- /dev/null +++ b/internal/dhcp_server/template/v4_config.tmpl @@ -0,0 +1,46 @@ +# Global Options +authoritative; +deny bootp; +deny declines; +one-lease-per-client on; + +# Servers +{{- range $i, $server := .Service.DHCPv4Servers }} +subnet {{ getInterfaceNetworkAddress $ $server.Interface }} netmask {{ getInterfaceNetworkMask $ $server.Interface }} { + # Pool + {{- range $j, $p := $server.Pool }} + range {{ getAddressObjectAsPoolRange $ $p }}; + {{- end}} + + # Settings + default-lease-time {{ getTimeInSecond $server.DefaultLeaseTime }}; + max-lease-time {{ getTimeInSecond $server.MaxLeaseTime }}; + + # Options + option subnet-mask {{ getInterfaceNetworkMask $ $server.Interface }}; + option broadcast-address {{ getInterfaceBroadcastAddress $ $server.Interface }}; + {{- if eq $server.GatewayMode 1 }} + option routers {{ getInterfaceAddress $ $server.Interface }}; + {{- else if eq $server.GatewayMode 2 }} + option routers {{ $server.Gateway }}; + {{- end }} + {{- if eq $server.DNSServerMode 1 }} + option domain-name-servers {{ getInterfaceAddress $ $server.Interface }}; + {{- else if eq $server.DNSServerMode 2 }} + option domain-name-servers {{ getAddressObjectsAsCommaList $ $server.DNSServers }}; + {{- end }} + {{- if eq $server.NTPServerMode 1 }} + option time-servers {{ getInterfaceAddress $ $server.Interface }}; + {{- else if eq $server.NTPServerMode 2 }} + option time-servers {{ getAddressObjectsAsCommaList $ $server.NTPServers }}; + {{- end }} + + # Hosts + {{- range $j, $reservation := $server.Reservations }} + host {{ $j }} { + hardware ethernet $reservation.HardwareAddress; + fixed-address $reservation.IPAddress; + } + {{end}} +} +{{end}} \ No newline at end of file diff --git a/internal/dhcp_server/template/v6_config.tmpl b/internal/dhcp_server/template/v6_config.tmpl new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go index 0f84f76..f4c190a 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "nfsense.net/nfsense/internal/api/object" "nfsense.net/nfsense/internal/api/service" "nfsense.net/nfsense/internal/config" + dhcp "nfsense.net/nfsense/internal/dhcp_server" "nfsense.net/nfsense/internal/jsonrpc" "nfsense.net/nfsense/internal/networkd" "nfsense.net/nfsense/internal/server" @@ -36,7 +37,7 @@ func main() { defer dbusConn.Close() configManager := config.CreateConfigManager() - configManager.RegisterApplyFunction(networkd.ApplyNetworkdConfiguration) + RegisterApplyFunctions(configManager) err = configManager.LoadCurrentConfigFromDisk() if err != nil { @@ -99,3 +100,8 @@ func RegisterAPIMethods(apiHandler *jsonrpc.Handler, configManager *config.Confi apiHandler.Register("Object", &object.Object{ConfigManager: configManager}) apiHandler.Register("Service", &service.Service{ConfigManager: configManager, DbusConn: dbusConn}) } + +func RegisterApplyFunctions(configManager *config.ConfigManager) { + configManager.RegisterApplyFunction(networkd.ApplyNetworkdConfiguration) + configManager.RegisterApplyFunction(dhcp.ApplyDHCPServerConfiguration) +}