diff --git a/api/v1beta1/firewallgroup_types.go b/api/v1beta1/firewallgroup_types.go index c662578..d6b93e9 100644 --- a/api/v1beta1/firewallgroup_types.go +++ b/api/v1beta1/firewallgroup_types.go @@ -36,6 +36,7 @@ type FirewallGroupSpec struct { // ManualAddresses is a list of manual IPs or CIDRs (IPv4 or IPv6) // +optional ManualAddresses []string `json:"manualAddresses,omitempty"` + ManualPorts []string `json:"manualPorts,omitempty"` // AutoIncludeSelector defines which services to extract addresses from // +optional diff --git a/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml b/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml index f54583f..b9cf679 100644 --- a/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml +++ b/config/crd/bases/unifi.engen.priv.no_firewallgroups.yaml @@ -99,6 +99,10 @@ spec: items: type: string type: array + manualPorts: + items: + type: string + type: array matchServicesInAllNamespaces: type: boolean name: diff --git a/internal/controller/firewallgroup_controller.go b/internal/controller/firewallgroup_controller.go index 0133f9d..b17e603 100644 --- a/internal/controller/firewallgroup_controller.go +++ b/internal/controller/firewallgroup_controller.go @@ -21,19 +21,21 @@ import ( "fmt" "net" "reflect" + "slices" + "strconv" "strings" + "regexp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/handler" -// "sigs.k8s.io/controller-runtime/pkg/source" - + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + // "sigs.k8s.io/controller-runtime/pkg/source" goUnifi "github.com/vegardengen/go-unifi/unifi" unifiv1beta1 "github.com/vegardengen/unifi-network-operator/api/v1beta1" @@ -69,7 +71,7 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R return reconcile.Result{}, client.IgnoreNotFound(err) } log.Info(nwObj.Spec.Name) - var ipv4, ipv6 []string + var ipv4, ipv6, tcpports, udpports []string for _, addressEntry := range nwObj.Spec.ManualAddresses { ip := net.ParseIP(addressEntry) @@ -99,6 +101,26 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R } } } + + for _, portEntry := range nwObj.Spec.ManualPorts { + port_type := "tcp" + port := portEntry + if match, _ := regexp.MatchString("(?:tcp|udp)\\/?)\\d+", string(portEntry)); match { + fields := strings.Split("/",portEntry) + port_type = fields[0] + port = fields[1] + } + if(port_type == "tcp") { + if !slices.Contains(tcpports, port) { + tcpports = append(tcpports, port) + } + } + if(port_type == "udp") { + if !slices.Contains(udpports, port) { + tcpports = append(udpports, port) + } + } + } var services corev1.ServiceList if nwObj.Spec.MatchServicesInAllNamespaces { if err := r.List(ctx, &services); err != nil { @@ -112,7 +134,7 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R return reconcile.Result{}, err } } - for _, service := range services.Items { + for _, service := range services.Items { if val, found := service.Annotations["unifi.engen.priv.no/firewall-group"]; found && val == nwObj.Name && service.Status.LoadBalancer.Ingress != nil { for _, ingress := range service.Status.LoadBalancer.Ingress { if ingress.IP != "" { @@ -120,7 +142,23 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R if isIPv6(ip) { ipv6 = append(ipv6, ip) } else { - ipv4= append(ipv4, ip) + ipv4 = append(ipv4, ip) + } + } + } + if service.Spec.Ports != nil { + for _, portSpec := range service.Spec.Ports { + log.Info(fmt.Sprintf("portSpec: %+v", portSpec)) + log.Info(fmt.Sprintf("Port: %s %d", strconv.Itoa(int(portSpec.Port)), portSpec.Port)) + if(portSpec.Protocol == "TCP") { + if !slices.Contains(tcpports, strconv.Itoa(int(portSpec.Port))) { + tcpports = append(tcpports, strconv.Itoa(int(portSpec.Port))) + } + } + if(portSpec.Protocol == "UDP") { + if !slices.Contains(udpports, strconv.Itoa(int(portSpec.Port))) { + udpports = append(udpports, strconv.Itoa(int(portSpec.Port))) + } } } } @@ -129,7 +167,7 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R nwObj.Status.ResolvedAddresses = ipv4 nwObj.Status.ResolvedAddresses = append(nwObj.Status.ResolvedAddresses, ipv6...) currentTime := metav1.Now() - nwObj.Status.LastSyncTime = ¤tTime; + nwObj.Status.LastSyncTime = ¤tTime nwObj.Status.SyncedWithUnifi = true err := r.UnifiClient.Reauthenticate() @@ -143,8 +181,12 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R } ipv4_name := "k8s-" + nwObj.Spec.Name + "-ipv4" ipv6_name := "k8s-" + nwObj.Spec.Name + "-ipv6" + tcpports_name := "k8s-" + nwObj.Spec.Name + "-tcpports" + udpports_name := "k8s-" + nwObj.Spec.Name + "-udpports" ipv4_done := false ipv6_done := false + tcpports_done := false + udpports_done := false for _, firewall_group := range firewall_groups { if firewall_group.Name == ipv4_name { if len(ipv4) == 0 { @@ -216,6 +258,76 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R ipv6_done = true } } + if firewall_group.Name == tcpports_name { + if len(tcpports) == 0 { + log.Info(fmt.Sprintf("Delete %s", tcpports_name)) + err := r.UnifiClient.Client.DeleteFirewallGroup(context.Background(), r.UnifiClient.SiteID, firewall_group.ID) + if err != nil { + msg := strings.ToLower(err.Error()) + log.Info(msg) + if strings.Contains(msg, "api.err.objectreferredby") { + log.Info("Firewall group is in use. Invoking workaround...!") + firewall_group.GroupMembers = []string{"127.0.0.1"} + firewall_group.Name = firewall_group.Name + "-deleted" + _, updateerr := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if updateerr != nil { + log.Error(updateerr, "Could neither delete or rename firewall group") + return reconcile.Result{}, updateerr + } + } else { + log.Error(err, "Could not delete firewall group") + return reconcile.Result{}, err + } + } + tcpports_done = true + } else { + if !reflect.DeepEqual(firewall_group.GroupMembers, tcpports) { + firewall_group.GroupMembers = tcpports + log.Info(fmt.Sprintf("Updating %s", tcpports_name)) + _, err := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err, "Could not update firewall group") + return reconcile.Result{}, err + } + } + tcpports_done = true + } + } + if firewall_group.Name == udpports_name { + if len(udpports) == 0 { + log.Info(fmt.Sprintf("Delete %s", udpports_name)) + err := r.UnifiClient.Client.DeleteFirewallGroup(context.Background(), r.UnifiClient.SiteID, firewall_group.ID) + if err != nil { + msg := strings.ToLower(err.Error()) + log.Info(msg) + if strings.Contains(msg, "api.err.objectreferredby") { + log.Info("Firewall group is in use. Invoking workaround...!") + firewall_group.GroupMembers = []string{"127.0.0.1"} + firewall_group.Name = firewall_group.Name + "-deleted" + _, updateerr := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if updateerr != nil { + log.Error(updateerr, "Could neither delete or rename firewall group") + return reconcile.Result{}, updateerr + } + } else { + log.Error(err, "Could not delete firewall group") + return reconcile.Result{}, err + } + } + udpports_done = true + } else { + if !reflect.DeepEqual(firewall_group.GroupMembers, udpports) { + firewall_group.GroupMembers = udpports + log.Info(fmt.Sprintf("Updating %s", udpports_name)) + _, err := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err, "Could not update firewall group") + return reconcile.Result{}, err + } + } + udpports_done = true + } + } if firewall_group.Name == ipv4_name+"-deleted" && len(ipv4) > 0 { firewall_group.Name = ipv4_name firewall_group.GroupMembers = ipv4 @@ -238,6 +350,28 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R } ipv6_done = true } + if firewall_group.Name == tcpports_name+"-deleted" && len(tcpports) > 0 { + firewall_group.Name = tcpports_name + firewall_group.GroupMembers = tcpports + log.Info(fmt.Sprintf("Creating %s (from previously deleted)", tcpports_name)) + _, err := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err, "Could not update firewall group") + return reconcile.Result{}, err + } + tcpports_done = true + } + if firewall_group.Name == udpports_name+"-deleted" && len(udpports) > 0 { + firewall_group.Name = udpports_name + firewall_group.GroupMembers = udpports + log.Info(fmt.Sprintf("Creating %s (from previously deleted)", udpports_name)) + _, err := r.UnifiClient.Client.UpdateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err, "Could not update firewall group") + return reconcile.Result{}, err + } + udpports_done = true + } } if len(ipv4) > 0 && !ipv4_done { log.Info(fmt.Sprintf("Creating %s", ipv4_name)) @@ -265,12 +399,40 @@ func (r *FirewallGroupReconciler) Reconcile(ctx context.Context, req reconcile.R return reconcile.Result{}, err } } - if err := r.Status().Update(ctx, &nwObj); err != nil { + if len(tcpports) > 0 && !tcpports_done { + log.Info(fmt.Sprintf("Creating %s", tcpports_name)) + var firewall_group goUnifi.FirewallGroup + firewall_group.Name = tcpports_name + firewall_group.SiteID = r.UnifiClient.SiteID + firewall_group.GroupMembers = tcpports + firewall_group.GroupType = "port-group" + log.Info(fmt.Sprintf("Trying to apply: %+v", firewall_group)) + _, err := r.UnifiClient.Client.CreateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err, "Could not create firewall group") + return reconcile.Result{}, err + } + } + if len(udpports) > 0 && !udpports_done { + log.Info(fmt.Sprintf("Creating %s", udpports_name)) + var firewall_group goUnifi.FirewallGroup + firewall_group.Name = udpports_name + firewall_group.SiteID = r.UnifiClient.SiteID + firewall_group.GroupMembers = udpports + firewall_group.GroupType = "port-group" + log.Info(fmt.Sprintf("Trying to apply: %+v", firewall_group)) + _, err := r.UnifiClient.Client.CreateFirewallGroup(context.Background(), r.UnifiClient.SiteID, &firewall_group) + if err != nil { + log.Error(err, "Could not create firewall group") + return reconcile.Result{}, err + } + } + if err := r.Status().Update(ctx, &nwObj); err != nil { log.Error(err, "unable to update FirewallGroup status") return reconcile.Result{}, err } - log.Info("Successfully updated FirewallGroup status with collected IP addresses") + log.Info("Successfully updated FirewallGroup status with collected IP addresses and ports") return reconcile.Result{}, nil } @@ -278,43 +440,44 @@ func isIPv6(ip string) bool { return strings.Contains(ip, ":") } func (r *FirewallGroupReconciler) mapServiceToFirewallGroups(ctx context.Context, obj client.Object) []reconcile.Request { - var requests []reconcile.Request - service, ok := obj.(*corev1.Service) - if !ok { - return requests - } + var requests []reconcile.Request + service, ok := obj.(*corev1.Service) + if !ok { + return requests + } - var allFirewallGroups unifiv1beta1.FirewallGroupList + var allFirewallGroups unifiv1beta1.FirewallGroupList - if err := r.List(ctx, &allFirewallGroups); err != nil { - return nil - } + if err := r.List(ctx, &allFirewallGroups); err != nil { + return nil + } - for _, fwg := range allFirewallGroups.Items { - if fwg.Spec.MatchServicesInAllNamespaces || fwg.Namespace == service.Namespace { - annotationKey := "unifi.engen.priv.no/firewall-group" - annotationVal := fwg.Name - if val, ok := service.Annotations[annotationKey]; ok && (annotationVal == "" || val == annotationVal) { - requests = append(requests, ctrl.Request{ - NamespacedName: types.NamespacedName{ - Name: fwg.Name, - Namespace: fwg.Namespace, - }, - }) - } - } - } + for _, fwg := range allFirewallGroups.Items { + if fwg.Spec.MatchServicesInAllNamespaces || fwg.Namespace == service.Namespace { + annotationKey := "unifi.engen.priv.no/firewall-group" + annotationVal := fwg.Name + if val, ok := service.Annotations[annotationKey]; ok && (annotationVal == "" || val == annotationVal) { + requests = append(requests, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: fwg.Name, + Namespace: fwg.Namespace, + }, + }) + } + } + } - return requests + return requests } + // SetupWithManager sets up the controller with the Manager. func (r *FirewallGroupReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&unifiv1beta1.FirewallGroup{}). - Named("firewallgroup"). - Watches( - &corev1.Service{}, - handler.EnqueueRequestsFromMapFunc(r.mapServiceToFirewallGroups), - ). - Complete(r) + return ctrl.NewControllerManagedBy(mgr). + For(&unifiv1beta1.FirewallGroup{}). + Named("firewallgroup"). + Watches( + &corev1.Service{}, + handler.EnqueueRequestsFromMapFunc(r.mapServiceToFirewallGroups), + ). + Complete(r) }