package ble import ( "context" "fmt" "os/exec" "time" "github.com/godbus/dbus/v5" ) const ( agentPath = "/org/bluez/agent/noinputnooutput" agentInterface = "org.bluez.Agent1" agentManager = "org.bluez.AgentManager1" bluezService = "org.bluez" bluezPath = "/org/bluez" ) // NoInputNoOutputAgent implements org.bluez.Agent1 with NoInputNoOutput capability. // This disables pairing prompts and allows "Just Works" pairing. type NoInputNoOutputAgent struct{} // Release is called when the agent is unregistered. func (a *NoInputNoOutputAgent) Release() *dbus.Error { return nil } // RequestPinCode returns an error as PIN codes are not supported with NoInputNoOutput. func (a *NoInputNoOutputAgent) RequestPinCode(device dbus.ObjectPath) (string, *dbus.Error) { return "", dbus.NewError("org.bluez.Error.Rejected", []any{"NoInputNoOutput agent"}) } // DisplayPinCode does nothing as we have no display. func (a *NoInputNoOutputAgent) DisplayPinCode(device dbus.ObjectPath, pincode string) *dbus.Error { return nil } // RequestPasskey returns an error as passkeys are not supported with NoInputNoOutput. func (a *NoInputNoOutputAgent) RequestPasskey(device dbus.ObjectPath) (uint32, *dbus.Error) { return 0, dbus.NewError("org.bluez.Error.Rejected", []any{"NoInputNoOutput agent"}) } // DisplayPasskey does nothing as we have no display. func (a *NoInputNoOutputAgent) DisplayPasskey(device dbus.ObjectPath, passkey uint32, entered uint16) *dbus.Error { return nil } // RequestConfirmation auto-accepts pairing for "Just Works" mode. func (a *NoInputNoOutputAgent) RequestConfirmation(device dbus.ObjectPath, passkey uint32) *dbus.Error { return nil // Accept pairing silently } // RequestAuthorization auto-accepts authorization for "Just Works" mode. func (a *NoInputNoOutputAgent) RequestAuthorization(device dbus.ObjectPath) *dbus.Error { return nil // Accept authorization silently } // AuthorizeService auto-accepts service authorization. func (a *NoInputNoOutputAgent) AuthorizeService(device dbus.ObjectPath, uuid string) *dbus.Error { return nil // Accept service authorization silently } // Cancel is called when an operation is canceled. func (a *NoInputNoOutputAgent) Cancel() *dbus.Error { return nil } // RegisterNoInputNoOutputAgent connects to system DBus and registers a NoInputNoOutput // agent with BlueZ. This disables pairing prompts for BLE connections. func RegisterNoInputNoOutputAgent() error { conn, err := dbus.SystemBus() if err != nil { return fmt.Errorf("failed to connect to system DBus: %w", err) } agent := &NoInputNoOutputAgent{} // Export the agent object on DBus err = conn.Export(agent, agentPath, agentInterface) if err != nil { return fmt.Errorf("failed to export agent: %w", err) } // Get the AgentManager interface agentMgr := conn.Object(bluezService, bluezPath) // Register our agent with NoInputNoOutput capability call := agentMgr.Call(agentManager+".RegisterAgent", 0, dbus.ObjectPath(agentPath), "NoInputNoOutput") if call.Err != nil { return fmt.Errorf("failed to register agent: %w", call.Err) } // Request to be the default agent call = agentMgr.Call(agentManager+".RequestDefaultAgent", 0, dbus.ObjectPath(agentPath)) if call.Err != nil { return fmt.Errorf("failed to set default agent: %w", call.Err) } return nil } // SetAdapterNotPairable sets the Pairable property to false on the default adapter. // This is optional and prevents the adapter from initiating pairing. func SetAdapterNotPairable() error { conn, err := dbus.SystemBus() if err != nil { return fmt.Errorf("failed to connect to system DBus: %w", err) } // Get the default adapter (hci0) adapter := conn.Object(bluezService, "/org/bluez/hci0") // Set Pairable to false using org.freedesktop.DBus.Properties interface call := adapter.Call("org.freedesktop.DBus.Properties.Set", 0, "org.bluez.Adapter1", "Pairable", dbus.MakeVariant(false)) if call.Err != nil { return fmt.Errorf("failed to set Pairable: %w", call.Err) } return nil } // DisableBonding runs btmgmt to disable bonding on the default adapter. // This prevents phones from initiating pairing requests. func DisableBonding() error { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() cmd := exec.CommandContext(ctx, "btmgmt", "-i", "0", "bondable", "off") output, err := cmd.CombinedOutput() if ctx.Err() == context.DeadlineExceeded { return fmt.Errorf("btmgmt timed out") } if err != nil { return fmt.Errorf("failed to disable bonding: %w (output: %s)", err, output) } return nil }