6 KiB
AGENTS.md
What this is
A standalone LDAPv3 read-only bridge in front of a NetAuth server
(github.com/netauth/netauth). It translates LDAP bind/search operations into
NetAuth RPC calls via the netauth/protocol protobuf types. It is not a full
LDAP server and never stores data; it only reshapes and serves live NetAuth
data. Single static binary (ldapd), intended to run bound to localhost.
There are no tests anywhere in this repo (no *_test.go files). Do not
assume go test ./... validates anything.
Commands
go build ./... # build
go vet ./...
go run . # run locally (reads config like the real binary, see below)
go mod vendor # repo vendors deps; vendor/ and dist/ are gitignored
# Production-style build (matches Dockerfile)
CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o ldapd .
Release is via goreleaser triggered by v* tags in
.github/workflows/release.yml; the before-hooks
run go mod vendor and scripts/prepare.sh (regenerates NOTICE from vendored
license files via scripts/vendor-licenses, which needs the vendor tree present).
Note dist/config.yaml is a generated/stale goreleaser artifact, not the config
actually used.
Layout
main.go # wiring only: config, logging, client, serve
internal/ldap/ # the LDAP server (package ldap)
ldap.go # New(), routing, SetDomain(), Serve/ServeTLS
type.go # server struct + naClient interface (the seam for fakes)
option.go # functional options: WithLogger, WithNetAuth, WithAnonBind
bind.go # bind handler (auth against NetAuth)
search.go # RootDSE, entity & group search handlers
bleve_mapper.go # LDAP filter -> Bleve query string translation
util.go # DN parsing (entityIDFromDN)
internal/buildinfo/ # Version var ("dev"), advertised in RootDSE
Architecture: main builds a viper config + netauth client, constructs the
server with functional options, and calls Serve/ServeTLS. The underlying
LDAP engine is a RouteMux (net/http-style routing on LDAP ops) with handlers
keyed by base DN and scope.
Non-obvious gotchas
- Forked LDAP libraries: the server stack is
github.com/ps78674/ldapserver+github.com/ps78674/goldap, not the more commonnwaples/ldapserver/go-ldappackages. Import asldap "github.com/ps78674/ldapserver". Check the vendored API before assuming upstream signatures. - Routes depend on the domain:
New()only registers NotFound/Abandon/Bind and the RootDSE route. Theou=entities/ou=groupssearch routes are registered as a side effect ofSetDomain(), which must be called afterNew()or searches won't route at all. Calling it twice re-registers routes. - Base DN construction:
SetDomaintakes a plain domain (example.com), splits on., prependsdc=netauth, yieldingdc=netauth,dc=example,dc=com. Tree layout under it is fixed:uid=<id>,ou=entities,<base>andcn=<name>,ou=groups,<base>. - Supported LDAP filters are extremely limited (
bleve_mapper.go): only equality match, OR, and attribute-present filters; only the attributesuid(→ID:) andcn(→Name:); values are passed through as raw Bleve query strings to NetAuth's search. Everything else returnsunwillingToPerform/"Filter type not supported". - Anonymous bind fall-through (
bind.go): whenallowAnonis enabled and the bind DN is empty, a success response is written but the handler does not return — execution continues intoentityIDFromDN(""), which errors and writes a second response. This appears to be a bug; be careful not to "fix" it blindly if behavior parity matters, and definitely add a response write guard if you touch it. - Only simple bind — no SASL; anything else is rejected outright.
- DN parsing known defect:
entityIDFromDNsplits on,without handling escaped commas (documented as acceptable since NetAuth IDs can't contain commas). Error strings there have typos ("is underneath", "must be rooted"); leave them alone unless asked. - Search fan-out: each entity result triggers an extra
EntityGroupsRPC formemberOf, and each group result triggersGroupMembersformember. A broad search is N+1 RPCs against NetAuth. Group expansion is done server-side by NetAuth (flattened, fully expanded groups). - Logging is dual-library: app logging uses
hashicorp/go-hclog; the vendored ldapserver useslogrus, andmain.gohijacks the global logrus logger to funnel protocol traces into hclog at forced Trace level viaNETAUTH_LOGLEVEL. Uses.l(hclog, keyed with"key", valuepairs) inside the server package. - NetAuth dependency seam: server code talks to NetAuth only through the
naClientinterface intype.go:12. If you add a NetAuth call, add it to that interface, not directly to the concrete client. - Version injection mismatch: goreleaser ldflags set
main.versionetc., but the RootDSE reportsinternal/buildinfo.Version, which stays"dev"in built binaries.
Configuration
viper-driven; config file config.{yaml,...} searched in /etc/netauth/,
$HOME/.netauth/, . — a missing config file is fatal (exit 5), even with
all values provided by env. Env vars use the NETAUTH_ prefix with _
replacing . (e.g. NETAUTH_LDAP_BIND, NETAUTH_LDAP_DOMAIN,
NETAUTH_LOGLEVEL). Known keys (defaults in main.go:16-22):
ldap.bind (localhost:389), ldap.tls (false), ldap.key, ldap.cert,
ldap.allow_anon (false), ldap.domain (no default — required).
Style notes
- Commit message convention:
component: Capitalized description(e.g.internal/ldap: Add option to bind anonmyously). - Handlers are methods on
*servernamedhandle<X>; response pattern is construct response →w.Write(res)on every exit path. - Loop style favors
for i := range xthroughout.