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

  1. Regenerate a matching keypair with `./nhp-acd keygen --curve` (or --sm2) and update config.toml
  2. Verify the key byte length matches the cipher scheme selected in config
  3. Ensure the value is the private key, not the public key, and of the same scheme as peers expect
  4. 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

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


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 the

View on GitHub (pinned to 6e04ca5ff0)