nexthop unreachable via iBGP

I have a test network set up as a sqare

A1 --- B1
 |     |
A2 --- B2

A1 and A2 are in AS1
B1 and B2 are in AS2

Each link has a /30 p2p address

A1 and A2 have an ibgp session between the two /30 IIPs
B1 and B2 have an ibgp session
A1 and B1 have an ebgp session with local pref of 100
A2 and B2 have an ebgp session with local pref of 30

Each router has a loopback

Due to the localpref, under normal circumstances traffic should travel
A2–A1–B1–B2, and B2–B1–A1–A2

The A2–B2 link should only be used if the A1–B1 link goes down

However if I look at the routes on A2, I see

0  Db  dst-address=192.168.0.4/32 gateway=1.1.1.2 gateway-status=1.1.1.2 unreachable distance=200 scope=40 target-scope=30 bgp-as-path="65002" 
        bgp-local-pref=100 bgp-origin=incomplete received-from=ibgp 
1 ADb  dst-address=192.168.0.4/32 gateway=1.1.2.2 gateway-status=1.1.2.2 reachable via  ether8 distance=20 scope=40 target-scope=10 bgp-as-path="65002" 
        bgp-local-pref=30 bgp-origin=incomplete received-from=ebgp

192.168.0.4 is the loopback address of B2. 1.1.1.2 is the B1 interface pointing to A1, 1.1.2.2 is the B2 interface pointing to A2

The “A2-A1-B1-B2” route isn’t used because B2 is unreachable.

However ibgp does install the route (10.0.0.1 is the p2p link between A2 and A1)

 0 ADb  dst-address=1.1.1.0/30 gateway=10.0.0.1 gateway-status=10.0.0.1 reachable via  ether1 distance=200 scope=40 target-scope=30 bgp-local-pref=100 
        bgp-origin=incomplete received-from=ibgp 
 1  Db  dst-address=1.1.1.0/30 gateway=1.1.2.2 gateway-status=1.1.2.2 reachable via  ether8 distance=20 scope=40 target-scope=10 bgp-as-path="65002" 
        bgp-local-pref=30 bgp-origin=incomplete received-from=ebgp

If I add a static route to B2 of 1.1.1.0/30 via B1, I get

 0 A S  dst-address=1.1.1.0/30 gateway=10.0.0.1 gateway-status=10.0.0.1 reachable via  ether1 distance=1 scope=30 target-scope=10 
 1  Db  dst-address=1.1.1.0/30 gateway=10.0.0.1 gateway-status=10.0.0.1 reachable via  ether1 distance=200 scope=40 target-scope=30 bgp-local-pref=100 
        bgp-origin=incomplete received-from=ibgp 
 2  Db  dst-address=1.1.1.0/30 gateway=1.1.2.2 gateway-status=1.1.2.2 reachable via  ether8 distance=20 scope=40 target-scope=10 bgp-as-path="65002" 
        bgp-local-pref=30 bgp-origin=incomplete received-from=ebgp 
        
 0 ADb  dst-address=192.168.0.4/32 gateway=1.1.1.2 gateway-status=1.1.1.2 recursive via 10.0.0.1 ether1 distance=200 scope=40 target-scope=30 
        bgp-as-path="65002" bgp-local-pref=100 bgp-origin=incomplete received-from=ibgp 
 1  Db  dst-address=192.168.0.4/32 gateway=1.1.2.2 gateway-status=1.1.2.2 reachable via  ether8 distance=20 scope=40 target-scope=10 bgp-as-path="65002" 
        bgp-local-pref=30 bgp-origin=incomplete received-from=ebgp

And everything works as expected.

I’m assuming therefore that BGP doesn’t use ibgp routes as part of it’s “is next hop reachable” calculation

I believe what I need to do to do it properly (rather than static routes) is run OSPF inside the AS, including advertising the p2p connected route to the other AS, and run ibgp (between the loopbacks or the A1-B1 p2p)?

Yes, that is correct, you need to run IGP inside your AS.
For simpler setups you could also use nexthop-choice=force-self in bgp peer settings.