Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new pfudpproxy service intended to provide UDP fail-over forwarding for NetFlow/sFlow (and per PR text, IPFIX) traffic to a healthy fingerbank-collector backend in cluster deployments, replacing the prior keepalived/LVS-based approach.
Changes:
- Introduces a new Go-based UDP proxy (
pfudpproxy) with backend health checking and failover selection. - Integrates the new service into packaging, systemd, PacketFence service management, firewall rules, and the admin UI.
- Removes the keepalived-generated LVS UDP load-balancing configuration for NetFlow/sFlow ports.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| rpm/packetfence.spec | Packages the new systemd unit and pfudpproxy binary in RPM builds. |
| lib/pf/services/manager/pfudpproxy.pm | Adds PacketFence service manager wrapper for pfudpproxy (cluster-managed). |
| lib/pf/services/manager/keepalived.pm | Removes LVS UDP config generation; notes pfudpproxy now handles NetFlow/sFlow. |
| lib/pf/iptables.pm | Registers a new iptables service rule generator for pfudpproxy. |
| lib/pf/cmd/pf/service.pm | Exposes pfudpproxy in the pfcmd service help listing. |
| html/pfappserver/.../_components/index.js | Adds a UI toggle component export for pfudpproxy. |
| html/pfappserver/.../_components/TheForm.vue | Adds the pfudpproxy toggle to the Services configuration UI. |
| go/cmd/pfudpproxy/config.go | Loads VIP/backends from pfconfig and defines default ports/healthcheck defaults. |
| go/cmd/pfudpproxy/healthcheck.go | Implements HTTPS health checks against backends. |
| go/cmd/pfudpproxy/loadbalancer.go | Implements “first healthy backend” failover selection and backend updates. |
| go/cmd/pfudpproxy/main.go | Wires config loading, health checker, proxy startup, systemd notify/watchdog, refresh loop. |
| go/cmd/pfudpproxy/proxy.go | Implements UDP listening and forwarding to the selected backend. |
| debian/rules | Installs/enables the new packetfence-pfudpproxy unit for Debian packaging. |
| debian/packetfence-pfudpproxy.service | Debian service file link to the shared systemd unit. |
| config.mk | Adds pfudpproxy to the Go binaries built/installed by the build system. |
| conf/systemd/packetfence-pfudpproxy.service | New systemd unit definition for pfudpproxy. |
| conf/pf.conf.defaults | Adds default service toggle and binary path for pfudpproxy. |
| conf/documentation.conf | Adds documentation entries for services.pfudpproxy and services.pfudpproxy_binary. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
go/cmd/pfudpproxy/proxy.go
Outdated
| // Create destination address using backend's management IP and same port | ||
| dstAddr := fmt.Sprintf("%s:%d", backend.ManagementIP, port) | ||
| udpDstAddr, err := net.ResolveUDPAddr("udp", dstAddr) | ||
| if err != nil { | ||
| log.LoggerWContext(ctx).Error(fmt.Sprintf("Failed to resolve destination address %s: %s", | ||
| dstAddr, err.Error())) | ||
| return | ||
| } | ||
|
|
||
| // Create a new UDP connection for forwarding | ||
| // Using a new connection each time since NetFlow/sFlow are fire-and-forget | ||
| conn, err := net.DialUDP("udp", nil, udpDstAddr) | ||
| if err != nil { | ||
| log.LoggerWContext(ctx).Error(fmt.Sprintf("Failed to connect to backend %s: %s", | ||
| dstAddr, err.Error())) | ||
| return | ||
| } | ||
| defer conn.Close() |
There was a problem hiding this comment.
forwardPacket resolves the destination address and creates a new UDP socket (DialUDP) for every packet. For NetFlow/sFlow traffic this can become a major bottleneck and can exhaust ephemeral ports/file descriptors under load; reuse a socket per backend+port (or use a shared UDPConn with WriteToUDP) and cache the resolved UDPAddr.
| # Port 2055 - NetFlow (UDP) | ||
| util_safe_push( "-i $tint -p udp -m udp --dport 2055 --jump ACCEPT", $chains->{'filter'}{'INPUT'} ); | ||
| # Port 6343 - sFlow (UDP) | ||
| util_safe_push( "-i $tint -p udp -m udp --dport 6343 --jump ACCEPT", $chains->{'filter'}{'INPUT'} ); |
There was a problem hiding this comment.
iptables_pfudpproxy_rules opens UDP 2055/6343, but does not open UDP 4739 (IPFIX) even though fingerbank collector rules and the PR description mention IPFIX. If pfudpproxy is taking over this traffic, add the missing port (or clarify that IPFIX isn't supported).
| util_safe_push( "-i $tint -p udp -m udp --dport 6343 --jump ACCEPT", $chains->{'filter'}{'INPUT'} ); | |
| util_safe_push( "-i $tint -p udp -m udp --dport 6343 --jump ACCEPT", $chains->{'filter'}{'INPUT'} ); | |
| # Port 4739 - IPFIX (UDP) | |
| util_safe_push( "-i $tint -p udp -m udp --dport 4739 --jump ACCEPT", $chains->{'filter'}{'INPUT'} ); |
| healthChecker := NewHealthChecker(config, lb) | ||
| go healthChecker.Start(ctx) | ||
|
|
||
| // Create and start UDP proxy | ||
| proxy := NewUDPProxy(config, lb) | ||
| go proxy.Start(ctx) | ||
|
|
||
| // Notify systemd we're ready | ||
| daemon.SdNotify(false, "READY=1") | ||
|
|
There was a problem hiding this comment.
Systemd is notified READY=1 immediately after starting healthChecker/proxy goroutines, but there’s no signal that any UDP listener actually bound successfully. This can mark the unit as ready while it isn't forwarding; consider making proxy.Start return an error/status (or a readiness channel) and only call SdNotify(READY=1) once at least one listener is active.
go/cmd/pfudpproxy/loadbalancer.go
Outdated
| defer lb.mu.RUnlock() | ||
|
|
||
| backends := make([]*Backend, len(lb.backends)) | ||
| copy(backends, lb.backends) |
There was a problem hiding this comment.
GetAllBackends returns a new slice but the elements are still pointers to the shared backend structs. Callers can easily read/mutate fields without the load balancer lock, leading to races (as in HealthChecker); consider returning an immutable snapshot (copy structs, not pointers) or adding accessor methods so all reads happen under the mutex.
| copy(backends, lb.backends) | |
| for i, backend := range lb.backends { | |
| if backend == nil { | |
| continue | |
| } | |
| bCopy := *backend | |
| backends[i] = &bCopy | |
| } |
| func LoadConfig(ctx context.Context) (*ProxyConfig, error) { | ||
| config := &ProxyConfig{ | ||
| Ports: []int{PortNetFlow, PortSFlow}, | ||
| HealthCheckPort: getHealthCheckPort(ctx), |
There was a problem hiding this comment.
The proxy is advertised as handling NetFlow/sFlow/IPFIX (per PR description/NEWS), but the default Ports list only includes 2055 and 6343. If IPFIX support is intended, add 4739 (and/or make ports configurable); otherwise align the feature description/docs to only mention NetFlow/sFlow.
| // NewLoadBalancer creates a new load balancer with the given backends. | ||
| // Backends are ordered by their position in the slice (first = highest priority). | ||
| func NewLoadBalancer(backends []*Backend) *LoadBalancer { | ||
| return &LoadBalancer{ | ||
| backends: backends, | ||
| } | ||
| } | ||
|
|
||
| // GetPrimary returns the first healthy backend (failover mode). | ||
| // Returns nil if no healthy backend is available. | ||
| func (lb *LoadBalancer) GetPrimary() *Backend { | ||
| lb.mu.RLock() | ||
| defer lb.mu.RUnlock() | ||
|
|
||
| for _, backend := range lb.backends { | ||
| if backend.Healthy { | ||
| return backend | ||
| } | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
No unit tests are introduced for the new failover behavior (load balancer selection, backend update preservation, and health check status transitions). Other Go commands in this repo have *_test.go coverage (e.g. go/cmd/pfdhcp), so adding focused tests here would help prevent regressions.
| // Create HTTP client with TLS config that skips certificate verification | ||
| // (fingerbank-collector uses self-signed certificates) | ||
| transport := &http.Transport{ | ||
| TLSClientConfig: &tls.Config{ | ||
| InsecureSkipVerify: true, | ||
| }, | ||
| TLSHandshakeTimeout: 5 * time.Second, | ||
| } |
There was a problem hiding this comment.
Health checks unconditionally set InsecureSkipVerify: true. Even if fingerbank-collector commonly uses self-signed certs, this removes TLS authenticity entirely; consider wiring this to an explicit config toggle (or trusting a configured CA bundle) so deployments that can validate certs are not forced into insecure mode.
eafc10f to
8aa8066
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Close all listeners | ||
| for _, listener := range p.listeners { | ||
| if listener != nil { | ||
| listener.Close() | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Stop iterates over p.listeners without holding p.mu, while listenAndForward appends to p.listeners under p.mu. This can cause a data race/panic under the race detector and potentially miss closing a listener. Use the same mutex to protect reads/writes to p.listeners (e.g., copy the slice under lock before iterating).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Description
Fail-over reverse proxy to forward sflow/netflow/ipfix to one of the backend fingerbank-collector
Impacts
Cluster
Delete branch after merge
YES
Checklist
NEWS file entries
New Features