@Derelict:
Yeah that's not at all how it works. Put your VIP and outbound NAT rule on the outbound interface. OPT2 in this case if I am understanding correctly.
Thank you! That worked perfectly first time - after all this time of banging my head. In case anyone else is looking, this is what I did:
1. Set up a virtual IP of type "IP alias" (but perhaps some other types would have worked just as well) with the IP that I want the packet to look like it came from (192.168.2.10 in this example). The IP alias is set on the interface it will leave the router on, not the one it arrives into the router at (OPT2 in this example).
2. Set hybrid NAT (or if you prefer Manual/AON) and then add an Outbound NAT rule again on the same interface the packet will leave on (OPT2) with source = any (or whatever IP range the packet actually came from) and dest = the destination IP or its subnet or whatever (I used 192.168.2.0/24). Then set the translation address by choosing the virtual IP from step 1, in the drop-down box.
As far as I understand it in lay-terms, the misunderstanding is that outbound NAT seems to mean "outbound from the router", not "outbound from a given network into the router". Ambiguity of language, but what a headache. The packet, sent to its destination IP, travels in from OPT1 and is picked up by NAT when it's outgoing at OPT2 (the interface in the NAT rule). As the packet's src matches "any" and its dest matches the value entered in the NAT rule (192.168.2.0/24), its source is translated to be 192.168.2.10 as required.
Packet capture confirms it - when I ping as described in the 1st post, packet capture on the OPT1 interface shows a ping and reply from 192.168.1.2 -> 192.168.2.2, but packet capture on the OPT2 interface shows a ping and reply from 192.168.2.10 -> 192.168.2.2 as desired.
Thank you very much indeed. (Maybe this could be made clearer in the documentation as well?)