fix(opnsense): valid HAProxy config + From<&str> codegen cleanup #273
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/haproxy-issues"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Three fixes and a codegen refactor. Together they take
LoadBalancerScore(andOKDLoadBalancerScore) from "silently generates a broken HAProxyconfig" to "deploys a working HAProxy end-to-end on OPNsense" — and cleans up the call-site-by-call-site string-to-enum boilerplate the fixes
originally added.
1.
is_installedno longer swallows errors (83d9af2)LoadBalancerConfig::is_installedreturnedboolvia.is_ok(), so a timeout, DNS failure, or auth rejection all collapsed to "not installed"— the score would then try to install
os-haproxyon top of an unreachable firewall and fail in cascade further down the pipeline.Now returns
Result<bool, Error>, treating only HTTP 404 ("controller not found") as "not installed" and propagating every other error so thescore fails fast with a clear message.
2. HAProxy healthcheck/server fields set explicitly (
21035d2)configure_serviceused..Default::default()for most fields of the generated HAProxy structs, letting OPNsense's model defaults leakthrough:
http_host→localhost(every check sentHost: localhost)http_method→options(OPTIONS instead of the declared method)http_version→http10sslVerifyon real servers →1(broke self-signed backends)sslwas never propagated, so SSL-required checks like kube-apiserver/readyzon 6443 stayed plain HTTP and never reached theTLS listener.
Every field is now mapped from
LbHealthCheck/LbServerexplicitly: method viaHealthcheckHttpMethod, version pinned toNone(wire""=NONE),
http_hostcleared to an empty string,sslpropagated throughHealthcheckSsl,ssl/sslVerifypinned tofalseat the call site.3.
From<&str>for every generated enum (fc16e9f,ead76e7)The fix in §2 introduced eight string-match blocks in
configure_servicethat duplicated wire-value knowledge the codegen already has. Addressed in review.opnsense-codegen/src/codegen.rs::generate_enumnow emitsimpl From<&str>andimpl From<String>for every generated enum, right after the existing serde module. Lowercase-matches wire values (both sides lowercased so mixed-case wirevalues like
"SSLv3","CONNECT"work); unknown inputs fall through to the existingOther(String)variant that the codegen emits for forward-compat round-tripping.opnsense-api/src/generated/haproxy.rsregenerated — 153 enums × 2 = 306 new impl blocks. No hand edits.cargo run -p opnsense-codegen -- generate
--xml opnsense-codegen/vendor/plugins/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml
--output-dir opnsense-api/src/generated
--module-name haproxy
--api-key haproxy
cargo fmt -p opnsense-api
opnsense-config/src/modules/load_balancer.rs::configure_servicedrops eight string-match blocks in favor of one-liners:HealthcheckType::from(hc.check_type.as_str()),hc.http_method.as_deref().map(HealthcheckHttpMethod::from), etc.harmony/src/infra/opnsense/load_balancer.rs—SSL::SNIwas mapped to wire value"sslni", but OPNsense's wire value is"sslsni"(both forward and reverse directions fixed, lines 149 and 185). Before this refactorthe typo silently hit
HealthcheckSsl::Other("sslni"); the cleaner conversion made the bug obvious.Environment note: HAProxy fails to start on firewalls with the HTTP→HTTPS redirect enabled
If HAProxy refuses to start with
cannot bind socket (Address already in use) for [0.0.0.0:80], check whether OPNsense's built-in HTTP→HTTPS redirect is enabled (System → Settings → Administration). It runs lighttpd dual-stacked on[::]:80, which via v4-mapped addresses also owns IPv4 port 80 — invisible tosockstat -l4but enough to reject HAProxy's bind. Either disable the redirect or make sure nothing else holds*:80before applying the score. Not fixed in Harmony;it's an OPNsense-side knob.
An earlier attempt (
5e72777) switched the bind to the firewall's LAN IP to work around this, but was reverted ina196268after confirming the root cause was environment-specific rather than a general incompatibility.0.0.0.0binds workfine on any OPNsense where nothing else is squatting on the ports.
@@ -170,0 +183,4 @@.http_method.as_deref().map(|m| match m.to_lowercase().as_str() {"options" => HealthcheckHttpMethod::OptionsDefault,Instead of doing those ugly matches that depend on strings that we don't control here, implement
From &strtraits for those structs HealthCheckHttpMethod and the other similar above and below HealthcheckType and HealthcheckSsl and probably others around here. This is poor rust, feels like crappy python by a first year programmer. Do better.Addresses review feedback on the previous HAProxy field-default fixes: the eight match blocks in `configure_service` that mapped loose strings ("get", "tcp", "roundrobin", ...) to generated OPNsense enum variants were poor Rust — they duplicated the wire-value knowledge that the codegen already has, and any new enum variant in OPNsense meant editing every call site by hand. - `opnsense-codegen/src/codegen.rs::generate_enum` now emits `impl From<&str>` and `impl From<String>` for every generated enum, right after the existing serde module. Lowercase-matches wire values; unknown inputs fall through to the `Other(String)` variant the codegen already emits for forward-compat round-tripping. - `opnsense-api/src/generated/haproxy.rs` regenerated — 153 enums, 306 new impl blocks. No hand edits; re-run via `cargo run -p opnsense-codegen -- generate --xml opnsense-codegen/vendor/plugins/net/haproxy/src/opnsense/mvc/app/models/OPNsense/HAProxy/HAProxy.xml --output-dir opnsense-api/src/generated --module-name haproxy`. - `opnsense-config/src/modules/load_balancer.rs::configure_service` replaces eight string-match blocks with one-liners: `HealthcheckType::from(hc.check_type.as_str())` etc. - Drive-by: fixed a pre-existing typo at `harmony/src/infra/opnsense/load_balancer.rs:185` and the matching reverse at `:149` — `SSL::SNI` was mapped to `"sslni"`, but the OPNsense wire value is `"sslsni"`. Before this refactor the typo silently hit `HealthcheckSsl::Other("sslni")`; the cleaner conversion made the bug obvious so it's fixed here rather than left for a follow-up. Verification: - `cargo check -p harmony -p opnsense-config -p opnsense-api` clean - `cargo test -p harmony --lib okd::load_balancer` 6/6 pass - `cargo test -p opnsense-codegen` 22/22 pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>fix(haproxy): make LoadBalancer score work end-to-end on OPNsenseto fix(opnsense): valid HAProxy config + From<&str> codegen cleanup@@ -249,3 +238,1 @@"uri" => BackendAlgorithm::UriHashOnlyHttpMode,other => BackendAlgorithm::Other(other.to_string()),}),mode: Some(BackendMode::from(backend.mode.as_str())),MUCH better but still dumb. Why do we have to do mode from mode.as_str() ???? Two different types? There should be only one type, I'd rather pull the type from opnsense-api than have two types for no reason. If there is a major reason then implement a type1::from(type2) so we are type safe, just boilerplate, but safe boilerplate is better than passing strings around.
types??