Skip to content

Conversation

@ben-grande
Copy link

I noticed during preload tests that my Whonix qubes properties were being overridden. Setting a feature on the template is not the best option, seen that it doesn't consider multi-VM setups where there can be multiple disposable templates. The order is as follows:

  • None if current property is None;
  • Current property if it asks Netvm it is a Gateway, itself it it asks
    for a DVM and it is a Workstation or current property if it asks for a
    DVM and it is a Workstation;
  • Check for features in template chain; and
  • Fallback to hardcoded values

@ben-grande
Copy link
Author

Review from @adrelanos or @ArrayBolt3 on behavior?

Copy link
Contributor

@ArrayBolt3 ArrayBolt3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the general direction this is going in. There are some changes I'd like to discuss, but for the most part this looks good.

Comment on lines 71 to 120
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
default_dispvm = getattr(vm, "default_dispvm", None)
if not default_dispvm:
return
if not default_dispvm.features.check_with_template(
"whonix-ws", None
):
vm.default_dispvm = None
return
template = getattr(vm, 'template', None)
# look for appropriate default dispvm
if (
template is not None
and 'whonix-default-dispvm' in template.features

template_for_dispvms = getattr(vm, "template_for_dispvms", False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use some of the same logic that is used for choosing a DispVM for AppVMs here for templates as well? We could try to default to vm.template.name + "-dvm". and if that doesn't exist or isn't a Whonix-Workstation VM, we can try to default to "whonix-workstation-18-dvm" (or "whonix-workstation-17-dvm" for R4.2). Only if both of those fails should we fall back to "None", IMO.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +83 to +136
if template_for_dispvms and getattr(vm, "template", None):
# If VM is a DVM and it's template has a DVM that is not a
# Workstation, use itself.
template_default_dispvm = getattr(
vm.template, "default_dispvm", None
)
if (
template_default_dispvm
and not template_default_dispvm.features.check_with_template(
"whonix-ws"
)
):
vm.template.default_dispvm = vm

curr_default_dispvm = getattr(vm, "default_dispvm", None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this block should be removed. Modifying a VM's template's default_dispvm seems error prone, especially since we set the template's default_dispvm using other means above. We could end up with non-deterministic or at least confusing behavior I fear.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it is not elegant. It was done to ensure that templates have the correct configuration. They have the wrong configuration by default because the disposable template doesn't exist when the whonix-ws is set on the template.

There are other places where the isinstance(vm TemplateVM) code could be moved, such as the domain-load of the template, but it will have the same result, just being checked after the qubes.xml file is loaded every time. But it doesn't seem better to be there than here though, unless we are talking about enforcing it more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're saying I think, and your logic seems sound to me. But in this instance I don't quite see how the code will actually work - when the Whonix-Workstation template is first created, before the DispVM template is made, the Whonix-Workstation template will have its default DispVM set to None by the template-specific code above, won't it? If that happens, doesn't this block become a no-op, since template_default_dispvm will be None?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the Whonix-Workstation template is first created, before the DispVM template is made, the Whonix-Workstation template will have its default DispVM set to None by the template-specific code above, won't it?

Yes.

If that happens, doesn't this block become a no-op, since template_default_dispvm will be None?

Yes, I thought it is better to be None and stay None than use a non-Tor route.

Copy link
Contributor

@ArrayBolt3 ArrayBolt3 Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. I guess this code block is probably useful, though I do wish it could be changed so that it wasn't usually a no-op.

Idea: we want to usually respect a default_dispvm value of None, but in this instance it would be very useful to be able to override the None value once, to set up the default DispVM properly during initial setup, but not override the user's configuration going forward. While Qubes OS does provide a property_is_default call, this isn't useful because it only lets us distinguish between an explicitly set default-dvm option, or a default-dvm option that's been defaulted to.

Maybe we can use a qvm-feature that mark when the default DispVM has been properly set, which we can use here? Not sure if something like that would be useful for setting the NetVM too, maybe it would be?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ben-grande Does the idea above (using a qvm-feature to indicate when the DispVM is initialized) sound good to you? I think this is the only remaining issue possibly blocking this PR. I can experiment with it on my end if you need to use your time elsewhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Idea: we want to usually respect a default_dispvm value of None, but in this instance it would be very useful to be able to override the None value once, to set up the default DispVM properly during initial setup, but not override the user's configuration going forward.
...
Maybe we can use a qvm-feature that mark when the default DispVM has been properly set, which we can use here?

For context, the whole template section:

def get_template_dispvm(
    template: qubes.vm.templatevm.TemplateVM,
) -> str:
    feature = vm.features.check_with_template(
        "whonix-default-dispvm", None
    )
    if (
        feature
        and feature in app.domains
        and feature.features.get("whonix-ws", None)
    ):
        # If any VM in template chain has the special feature, use it.
        default_dispvm = feature
    elif template is not None:
        # If we have a template, use it for assuming a name.
        default_dispvm = template.name + "-dvm"
    else:
        # If all fails, use hardcoded name.
        # HARDCODED.
        default_dispvm = "whonix-workstation-17-dvm"
    return default_dispvm

if isinstance(vm, qubes.vm.templatevm.TemplateVM):
    default_dispvm = getattr(vm, "default_dispvm", None)
    if not default_dispvm:
        return
    if not default_dispvm.features.check_with_template(
        "whonix-ws", None
    ):
        set_default_dispvm(vm, get_template_dispvm(vm))
    return

template_for_dispvms = getattr(vm, "template_for_dispvms", False)
if template_for_dispvms and getattr(vm, "template", None):
    # If VM is a DVM and it's template has a DVM that is not a
    # Workstation, use itself.
    template_default_dispvm = getattr(
        vm.template, "default_dispvm", None
    )
    if (
        template_default_dispvm
        and not template_default_dispvm.features.check_with_template(
            "whonix-ws"
        )
    ):
        vm.template.default_dispvm = vm

The code runs when the qube is created or the feature whonix-ws was enabled.

The second block is for the template. If there are no suitable Whonix default disposable templates, it will be set to None. When creating descendants, the qube's default disposable template uses the system's default disposable template (qubes.vm.qubesvm.QubesVM.default_dispvm), thus it won't impact descendants.

The third block runs when an AppVM is created as a disposable template and its template's default disposable template is not None and is not a Workstation, thus setting the template's default disposable template to the AppVM (disposable template) itself. This happens when the template was created before this extension is upgraded. It won't set the template's default disposable template to None.

Not sure if something like that would be useful for setting the NetVM too, maybe it would be?

For the netvm, we are not changing the netvm of the template, only of the qube itself (as it shouldn't have a template anyway), so I don't understand how this relates.

Copy link
Contributor

@ArrayBolt3 ArrayBolt3 Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This happens when the template was created before this extension is upgraded. It won't set the template's default disposable template to None.

Right... but where I'm lost is that, in a situation where this updated qubes-core-admin-addon-whonix is the only version of the package that's ever been on the system, and templates are being created for the first time during system install, things don't seem to quite work as expected. First the template's default DispVM will be set to None (because that gets set up before the first DispVM template is created), then the first DispVM template based on the real template is created. At that point the real template's default DispVM is None, and so it will remain None until the user changes it, no? Therefore the code that is intended to set the real template's default DispVM to the newly created DispVM template ends up doing effectively nothing, and the real template is stuck with no default DispVM.

I guess I can see an argument for that being intended behavior (does a template need a default DispVM? Probably not usually, and users who do need that can set it up manually without much trouble). But it seems like it would be better if the real template did have a default DispVM as long as the user never explicitly set the default DispVM to None. That's why I suggested using a feature as a flag so that we could set the real template's default DispVM to that of the newly created DispVM template.

For the netvm, we are not changing the netvm of the template, only of the qube itself (as it shouldn't have a template anyway), so I don't understand how this relates.

It was a passing thought, I didn't really know if it related when I mentioned it, I just thought maybe it was worth bringing up. Neither of us can see how this would apply to NetVMs, so let's just ignore that.

Comment on lines 49 to 56
feature = vm.features.check_with_template("whonix-default-gw", None)
if feature:
# If any VM in template chain has the special feature, use it.
netvm = feature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be unsafe to assume that a VM with the feature whonix-default-gw is actually a Whonix-Gateway template. Worth double-checking the VM in feature for the whonix-gw feature?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 118 to 123
feature = vm.features.check_with_template(
"whonix-default-dispvm", None
)
if feature:
# If any VM in template chain has the special feature, use it.
default_dispvm = feature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to one of the comments above (dunno what order Github is going to put them in), we should probably ensure that the feature VM has the whonix-ws feature set.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +143 to 171
@staticmethod
def set_gw_dispvm(app, vm):
"""Set the default DispVM for a Whonix-Gateway qube to None if the
current one is not a Whonix-Workstation."""
# pylint: disable=unused-argument
default_dispvm = getattr(vm, "default_dispvm", None)
if not default_dispvm:
return
if not default_dispvm.features.check_with_template("whonix-ws", None):
vm.default_dispvm = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a good idea. It doesn't gain us anything since the Whonix-Gateway can already leak the user's identity on its own without root privileges, so this adds no privacy. I can see situations where someone might want to launch Whonix-Workstation DispVMs from sys-whonix, but the user can decide that without being denied the ability to use anything else.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the user can decide that without being denied the ability to use anything else.

We don't know if the default_dispvm currently set on Whonix Gateway was the user's choice or the system's default from installation, which is default-dvm.

I think that it is best to have this order:

  • whonix disposable template
  • None

If the user wants to use a different disposable template than the default one, they can do it:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anything triggers this function again in the future though, it will override the user's choice and set the default DispVM to None though. I'm pretty sure this function will get called every time dom0 is booted, so in practice any attempt to set a different DispVM will be ephemeral with this.

Are there any concrete risks with allowing Whonix-Gateway to have a default DispVM of default-dvm? I know there are with Whonix-Workstation, but I don't think there are with Whonix-Gateway (open-link-confirmation should prevent opening of links from Whonix-Gateway unless there's a mechanism in Qubes that bypasses this).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If anything triggers this function again in the future though, it will override the user's choice and set the default DispVM to None though. I'm pretty sure this function will get called every time dom0 is booted, so in practice any attempt to set a different DispVM will be ephemeral with this.

It is fired only once.

@qubes.events.handler("domain-add")
def on_domain_add_test(self, event):
    self.log.warning("TEST: domain-add")
sudo journalctl -fu qubesd
sudo systemctl restart qubesd

It won't be fired for all existing domains. Now try to add a domain and you will see a message.

Are there any concrete risks with allowing Whonix-Gateway to have a default DispVM of default-dvm? I know there are with Whonix-Workstation, but I don't think there are with Whonix-Gateway (open-link-confirmation should prevent opening of links from Whonix-Gateway unless there's a mechanism in Qubes that bypasses this).

The current Whonix Gateway model is:

  • Whonix Gateway tries to make everything from itself, not only it's clients, go through Tor, except the clearnet user

The default_dispvm can be used by any user, if user user wants to open a file on Whonix Gateway, for whatever reason, in a disposable and it does not go through Tor, it would be an exception to the model above.

Copy link
Contributor

@ArrayBolt3 ArrayBolt3 Oct 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fired only once.

Silly me got domain-add and domain-load mixed up. In that instance this is a good improvement. Sorry about the confusion.

Edit: I also didn't realize there were defenses against an identity leak if a user other than clearnet was compromised. That's awesome, and this is very much in line with that.

Comment on lines 119 to 213
if value == '1':
self.set_ws_netvm(vm.app, vm)
self.set_ws_dispvm(vm.app, vm)
if not value:
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't accept values other than 1 here. We may have users who are relying on non-1 values to say "no, this should not be treated as a Whonix-Workstation qube". Changing the semantics here breaks the API.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String matching is wrong, should never been queried this way on the code. On the readme, it says:

When a new VM is created based on a templte with whonix-ws feature set, it gets: ...`

So it never mentioned that setting other values should be used, instead, it should be set. Empty, not set or 0 will be interpreted as false. I believe this is the intended behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid point. I guess the question is then whether this should be considered a documentation bug, or a code bug, and whether some danger could surface from a VM being considered a Whonix-Workstation when it wasn't previously without user interaction. If a Whonix-Workstation qube stopped being considered a Whonix-Workstation, that could open the door for identity leaks, but if a non-Whonix-Workstation qube starts behaving as a Whonix-Workstation, that might be confusing and result in some configuration getting mangled, but it probably won't endanger the user.

@adrelanos Thoughts? If you're fine with the API technically breaking here, then I think this is good.

I noticed during preload tests that my Whonix qubes properties were
being overridden. Setting a feature on the template is not the best
option, seen that it doesn't consider multi-VM setups where there can be
multiple disposable templates. The order is as follows:

- None if current property is None;
- Current property if it asks Netvm it is a Gateway, itself it it asks
  for a disposable template and it is a Workstation or current property
  if it asks for a disposable template and it is a Workstation;
- Check for features in template chain; and
- Fallback to hardcoded values

Whonix Templates are a bit different, they are created with the global
default_dispvm, which is not Whonix by default, they are also created
before a disposable template (at least a descendant) can exist.
Workstation as the default disposable template, it will be kept,

For: QubesOS/qubes-issues#1512
Copy link
Author

@ben-grande ben-grande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, most of the things were accepted but the others that I disagreed, I am awaiting discussion.

Comment on lines +143 to 171
@staticmethod
def set_gw_dispvm(app, vm):
"""Set the default DispVM for a Whonix-Gateway qube to None if the
current one is not a Whonix-Workstation."""
# pylint: disable=unused-argument
default_dispvm = getattr(vm, "default_dispvm", None)
if not default_dispvm:
return
if not default_dispvm.features.check_with_template("whonix-ws", None):
vm.default_dispvm = None
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the user can decide that without being denied the ability to use anything else.

We don't know if the default_dispvm currently set on Whonix Gateway was the user's choice or the system's default from installation, which is default-dvm.

I think that it is best to have this order:

  • whonix disposable template
  • None

If the user wants to use a different disposable template than the default one, they can do it:

Comment on lines 118 to 123
feature = vm.features.check_with_template(
"whonix-default-dispvm", None
)
if feature:
# If any VM in template chain has the special feature, use it.
default_dispvm = feature
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 49 to 56
feature = vm.features.check_with_template("whonix-default-gw", None)
if feature:
# If any VM in template chain has the special feature, use it.
netvm = feature
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +83 to +136
if template_for_dispvms and getattr(vm, "template", None):
# If VM is a DVM and it's template has a DVM that is not a
# Workstation, use itself.
template_default_dispvm = getattr(
vm.template, "default_dispvm", None
)
if (
template_default_dispvm
and not template_default_dispvm.features.check_with_template(
"whonix-ws"
)
):
vm.template.default_dispvm = vm

curr_default_dispvm = getattr(vm, "default_dispvm", None)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it is not elegant. It was done to ensure that templates have the correct configuration. They have the wrong configuration by default because the disposable template doesn't exist when the whonix-ws is set on the template.

There are other places where the isinstance(vm TemplateVM) code could be moved, such as the domain-load of the template, but it will have the same result, just being checked after the qubes.xml file is loaded every time. But it doesn't seem better to be there than here though, unless we are talking about enforcing it more.

Comment on lines 71 to 120
if isinstance(vm, qubes.vm.templatevm.TemplateVM):
default_dispvm = getattr(vm, "default_dispvm", None)
if not default_dispvm:
return
if not default_dispvm.features.check_with_template(
"whonix-ws", None
):
vm.default_dispvm = None
return
template = getattr(vm, 'template', None)
# look for appropriate default dispvm
if (
template is not None
and 'whonix-default-dispvm' in template.features

template_for_dispvms = getattr(vm, "template_for_dispvms", False)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants