Wire WireGuard peer DNS to ControlD profiles via ctrld

When creating a WireGuard peer on OPNsense:
- Client config DNS now points to OPNsense's IP (not tunnel gateway)
  so DNS flows: client → OPNsense → Unbound → ctrld → ControlD
- New dns_profile field: select which ControlD profile applies to
  VPN clients (default: "house" for VLAN 99)
- Generates ctrld.toml instructions for WireGuard tunnel subnet
  routing — tells user what to add so ctrld routes VPN DNS queries
  to the correct ControlD profile
- QR modal now shows ControlD setup instructions alongside the
  WireGuard config

This solves the Android Private DNS conflict: WireGuard's DNS setting
overrides Android's Private DNS, pointing to OPNsense which runs
Unbound → ctrld. No Private DNS toggle needed on the phone.

Multi-VLAN access for VPN peers works because the peer is on the
WireGuard interface (not on any VLAN). OPNsense routes between the
tunnel and VLANs per firewall rules. VLAN isolation preserved.

https://claude.ai/code/session_01Do9bsN39MTuy2GVv7yzSrE
This commit is contained in:
Claude
2026-03-28 12:16:00 +00:00
parent e486de26b0
commit 31f2008153
2 changed files with 72 additions and 7 deletions
+32 -4
View File
@@ -1913,7 +1913,7 @@ function DeviceAccessTab({ session, onNeedAuth, backendOk }) {
// WIREGUARD TAB
//
function QRModal({ config, name, onClose }) {
function QRModal({ config, name, ctrldNote, onClose }) {
// Render QR using a simple API since we can't use native qrencode in browser
const [qrUrl, setQrUrl] = useState('');
useEffect(() => {
@@ -1936,6 +1936,14 @@ function QRModal({ config, name, onClose }) {
maxHeight:120,overflowY:"auto",marginBottom:12}}>
{config}
</div>
{ctrldNote && (
<div style={{background:"var(--b1)",borderRadius:4,padding:8,fontSize:10,
color:"var(--dm)",textAlign:"left",whiteSpace:"pre-wrap",
marginBottom:12,lineHeight:1.6}}>
<div style={{fontWeight:700,color:"var(--ac)",marginBottom:4}}>ControlD DNS Setup</div>
{ctrldNote}
</div>
)}
<div style={{display:"flex",gap:8,justifyContent:"center"}}>
<button className="btn bg" onClick={()=>{
const a=document.createElement('a');
@@ -2200,6 +2208,7 @@ function WireGuardTab({ session, onNeedAuth, backendOk, vlans = [] }) {
});
const [opnPeerName, setOpnPeerName] = useState('');
const [opnVlans, setOpnVlans] = useState([]); // checked VLAN IDs
const [opnDnsProfile, setOpnDnsProfile] = useState('house'); // ControlD profile for VPN clients
const [opnAdding, setOpnAdding] = useState(false);
const [opnQr, setOpnQr] = useState(null); // { config, name }
@@ -2294,9 +2303,10 @@ function WireGuardTab({ session, onNeedAuth, backendOk, vlans = [] }) {
const r = await API("/opnsense/wireguard/add-peer", {
method:"POST",
body:{ token: session.token, name: opnPeerName.trim(),
allowed_vlans: opnVlans, vlan_subnets }
allowed_vlans: opnVlans, vlan_subnets,
dns_profile: opnDnsProfile }
});
setOpnQr({ config: r.config, name: r.name });
setOpnQr({ config: r.config, name: r.name, ctrld_note: r.ctrld_note });
setOpnPeerName(''); setOpnVlans([]);
await loadOpnWg();
} catch(e) { setOpnError(e.message); }
@@ -2363,6 +2373,7 @@ function WireGuardTab({ session, onNeedAuth, backendOk, vlans = [] }) {
opnSetup={opnSetup} setOpnSetup={setOpnSetup}
opnPeerName={opnPeerName} setOpnPeerName={setOpnPeerName}
opnVlans={opnVlans} opnAdding={opnAdding}
opnDnsProfile={opnDnsProfile} setOpnDnsProfile={setOpnDnsProfile}
session={session} onNeedAuth={onNeedAuth} vlans={vlans}
onToggleVlan={opnToggleVlan} onSetupServer={opnSetupServer}
onDeleteServer={opnDeleteServer} onAddPeer={opnAddPeer}
@@ -2511,6 +2522,8 @@ function WireGuardTab({ session, onNeedAuth, backendOk, vlans = [] }) {
setOpnPeerName={setOpnPeerName}
opnVlans={opnVlans}
opnAdding={opnAdding}
opnDnsProfile={opnDnsProfile}
setOpnDnsProfile={setOpnDnsProfile}
session={session}
onNeedAuth={onNeedAuth}
vlans={vlans}
@@ -2524,7 +2537,8 @@ function WireGuardTab({ session, onNeedAuth, backendOk, vlans = [] }) {
/>
)}
{opnQr && <QRModal config={opnQr.config} name={opnQr.name} onClose={()=>setOpnQr(null)}/>}
{opnQr && <QRModal config={opnQr.config} name={opnQr.name}
ctrldNote={opnQr.ctrld_note} onClose={()=>setOpnQr(null)}/>}
</div>
);
}
@@ -2533,6 +2547,7 @@ function WireGuardTab({ session, onNeedAuth, backendOk, vlans = [] }) {
function OPNsenseWGSection({
opnWg, opnLoading, opnError, opnSetup, setOpnSetup,
opnPeerName, setOpnPeerName, opnVlans, opnAdding,
opnDnsProfile, setOpnDnsProfile,
session, onNeedAuth, vlans,
onToggleVlan, onSetupServer, onDeleteServer,
onAddPeer, onRevokePeer, onShowConf, onRefresh,
@@ -2779,6 +2794,19 @@ function OPNsenseWGSection({
</div>
</div>
<div style={{marginTop:10}}>
<div className="field" style={{margin:0,maxWidth:260}}>
<label>ControlD DNS Profile (applied via ctrld)</label>
<input value={opnDnsProfile}
onChange={e=>setOpnDnsProfile(e.target.value)}
placeholder="house"/>
</div>
<div style={{fontSize:11,color:"var(--dm)",marginTop:4}}>
VPN clients use OPNsense DNS Unbound ctrld ControlD.
This profile applies to the WireGuard tunnel subnet.
</div>
</div>
<div style={{marginTop:12,display:"flex",gap:8,alignItems:"center"}}>
{!session && (
<button className="btn bg" style={{fontSize:11,padding:"4px 10px"}}