How to dynamically manipulate canonical entity URLs
Is there a service specifically for generating canonical URLs I can
decorate?

Yes, for a route based handling of outgoing canonical links you can use OutboundRouteProcessorInterface::processOutbound
For example like core implements the dynamic route &lt;current&gt;.
/core/lib/Drupal/Core/RouteProcessor/RouteProcessorCurrent.php
  public function processOutbound($route_name, Route $route, array &amp;$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
    if ($route_name === '&lt;current&gt;') {
      if ($current_route = $this-&gt;routeMatch-&gt;getRouteObject()) {
        $requirements = $current_route-&gt;getRequirements();
        // Setting _method and _schema is deprecated since 2.7. Using
        // setMethods() and setSchemes() are now the recommended ways.
        unset($requirements['_method']);
        unset($requirements['_schema']);
        $route-&gt;setRequirements($requirements);

        $route-&gt;setPath($current_route-&gt;getPath());
        $route-&gt;setSchemes($current_route-&gt;getSchemes());
        $route-&gt;setMethods($current_route-&gt;getMethods());
        $route-&gt;setOptions($current_route-&gt;getOptions());
        $route-&gt;setDefaults($current_route-&gt;getDefaults());
        $parameters = array_merge($parameters, $this-&gt;routeMatch-&gt;getRawParameters()-&gt;all());
        if ($bubbleable_metadata) {
          $bubbleable_metadata-&gt;addCacheContexts(['route']);
        }
      }
      else {
        // If we have no current route match available, point to the frontpage.
        $route-&gt;setPath('/');
      }
    }
  }