OpenNHP/opennhp · critical
failed to create device
Error message
failed to create device %v
What it means
UdpAC.Start returns "failed to create device %v" when core.NewDevice(core.NHP_AC, prk, nil) returns nil after the private key successfully base64-decodes. NewDevice validates the key material for the device type and cipher scheme; nil means the decoded bytes are not a usable private key.
Solutions
- Regenerate a matching keypair with `./nhp-acd keygen --curve` (or --sm2) and update config.toml
- Verify the key byte length matches the cipher scheme selected in config
- Ensure the value is the private key, not the public key, and of the same scheme as peers expect
- Improve NewDevice to return an error describing why the key was rejected instead of nil
Example fix
// before
a.device = core.NewDevice(core.NHP_AC, prk, nil)
if a.device == nil {
return fmt.Errorf("failed to create device %v", err)
}
// after
a.device = core.NewDevice(core.NHP_AC, prk, nil)
if a.device == nil {
return fmt.Errorf("failed to create device: invalid %d-byte private key for selected cipher scheme", len(prk))
} Defensive patterns
Strategy: validation
Validate before calling
prk, err := base64.StdEncoding.DecodeString(conf.PrivateKeyBase64)
if err != nil { return err }
if len(prk) != 32 { // curve25519 expected size
return fmt.Errorf("private key is %d bytes, expected 32", len(prk))
} Try / catch
a.device = core.NewDevice(core.NHP_AC, prk, nil)
if a.device == nil {
return fmt.Errorf("device creation failed: check key length/scheme (got %d bytes)", len(prk))
} Prevention
- Match key scheme (curve vs SM2) to the configured cipher scheme
- Never reuse public keys in the private-key field
- Regenerate keypairs after scheme changes
When it happens
Trigger: PrivateKeyBase64 decodes as base64 but its byte length is wrong for the scheme (e.g. 32-byte curve key configured while GMSM/SM2 scheme selected, or truncated/garbage bytes); NewDevice's internal key generation/validation fails and returns nil.
Common situations: Rotating keys between curve25519 and SM2 without regenerating; provisioning scripts truncating the base64 string; copying a public key into the private-key field.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- private key parse error
- server peer config invalid on initial load
- config load error
- private key parse error
- failed to create device
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/858c1352c0ff303d.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/ac/udpac.go:142
err = ebpflocal.EbpfEngineLoad(dirPath, logLevel, a.config.ACId)
if err != nil {
return err
}
default:
log.Error("[HandleAccessControl] unsupported FilterMode: %d (expected 0=IPTABLES or 1=EBPFXDP)", a.config.FilterMode)
return
}
prk, err := base64.StdEncoding.DecodeString(a.config.PrivateKeyBase64)
if err != nil {
log.Error("private key parse error %v\n", err)
return fmt.Errorf("private key parse error %v", err)
}
a.device = core.NewDevice(core.NHP_AC, prk, nil)
if a.device == nil {
log.Critical("failed to create device %v\n", err)
return fmt.Errorf("failed to create device %v", err)
}
a.remoteConnectionMap = make(map[string]*UdpConn)
a.serverPeerMap = make(map[string]*core.UdpPeer)
a.tokenStore = common.NewTokenStore[*AccessEntry]()
if a.etcdConn != nil {
_ = a.loadRemoteConfig()
} else {
// load http config and turn on http server if needed
_ = a.loadHttpConfig()
// load peers. A non-nil error here means the initial
// expandServerPeers parse failed and the running peerMap is
// empty. Starting the daemon in that state lets it drop
// AOL/AOP traffic silently (no peer matches), which is much
// harder to diagnose than a startup refusal. Reloads still
// keep the previous peer table on parse error — that's theView on GitHub (pinned to 6e04ca5ff0)