Преглед изворни кода

Diagnostic: a macvlan container is on the LAN, not bridge networking

- The network mode check reads each interface's link kind (ip -d):
  macvlan and ipvlan L2 pass, with their own message, and the subnet
  check runs as on host networking
- ipvlan L3/L3S warns: no broadcast or multicast, so the slicer cannot
  find the Virtual Printer and printers need the subnet scan
- Link kinds that can't be read report the mode as unknown instead of
  guessing bridge
- The support package reads the network mode the same way
maziggy пре 1 дан
родитељ
комит
b5239e52a0

+ 1 - 0
CHANGELOG.md

@@ -205,6 +205,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows.
 
 ### Fixed
+- **The connection diagnostic no longer calls a macvlan container bridge networking (#3417, reported by @ddavidebor)** — A container on a Docker macvlan network has its own address on the LAN, so printer discovery and the Virtual Printer work, yet the diagnostic warned that it was on bridge networking and should be recreated with host networking. The interface is moved into the container from the host, like a bridge network's, and the check could not tell the two apart. It now reads the interface's link type: macvlan, and ipvlan in L2 mode, pass, and the subnet check runs as on host networking. ipvlan in L3 mode gets its own warning: it passes no broadcast or multicast, so the slicer can't find the Virtual Printer and printers can only be found with the subnet scan. Where the link type can't be read, the check now says the network mode could not be determined rather than guessing bridge. The support package reads the network mode the same way. Wiki updated.
 - **Recalculate Costs uses the spools' current prices (#3414, reported by @BurgerKerman)** — Changing a spool's price and pressing **Recalculate Costs** changed nothing ("0 archives were updated"), whether or not the spool had its own price. Each usage record keeps what its grams cost when the print completed, and the recalculation only added those up again, so it read the old price back. The usage records of spools still in the inventory now get the spool's current price first, or the current default filament cost for a spool without one, and the archive cost is rebuilt from them. Each print in the Print Log moves by the change of its own records, or takes the archive's new cost when it is the archive's only priced print, so the statistics, project totals and order sums follow the new price as well. A reprint off a spool whose price did not change keeps its cost. Records of a deleted spool keep what they cost. In Spoolman mode, charges recorded since #3407 are repriced from the spool's current price in Spoolman. If Spoolman can't be reached they keep their price, and the Statistics page says how many. Wiki updated.
 - **Spoolman mode gets the same usage history and stock forecast as built-in inventory (#3407, reported by @ojimpo)** — Bambuddy charges Spoolman for every print but kept no record of it, and Spoolman itself keeps only per-spool totals. So Spoolman mode had no per-print usage at all: the stock forecast could never use its **Trend** rate, and the spool dialog showed no usage history. Each charge is now recorded like a built-in spool's: grams, print, status, and the cost when the spool has a price. So the forecast reaches **Trend** once two days of prints are recorded, and the spool dialog lists its prints. The rows are part of the GitHub backup. The fallback rate used before that was also wrong in both modes. It divided a spool's grams by the days since it was added, so shelf time counted as use. A spool used up inside that span but last used more than 90 days ago dropped out with all its grams while its days stayed in. On the reporter's SKU it showed 9.2 g/d. Each spool's grams are now spread over the time it was actually in use, and what falls inside the last 90 days is divided by those days (12.2 g/d there). The stock alerts use the same rule. Wiki updated.
 - **A Spoolman slot change made elsewhere now reaches open pages without a reload (#3402, reported by @Eifel-Joe)** — In Spoolman mode, an AMS slot's hover card kept showing the spool assigned before when the change came from another tab or client, here a filament scale using the API. The page only learns about slot changes that the server announces, and unassigning a slot, linking or unlinking a spool's tag, adding a spool from a slot, and writing a tag on SpoolBuddy did not announce theirs. They do now, as built-in inventory always has. Assigning a slot has announced since the move-on-scan change (#1606), but did so before taking the slot's tag off the previous spool, so a page could read that spool back and keep its fill level on the slot. It now announces after.

+ 11 - 12
backend/app/api/routes/support.py

@@ -222,21 +222,20 @@ def _sanitize_path(path: str) -> str:
 
 
 def _detect_docker_network_mode() -> str:
-    """Detect Docker network mode by checking for host-level interfaces.
+    """The container's network mode as the connection diagnostic reads it:
+    "host", "bridge", "macvlan", "ipvlan", "ipvlan_l3", or "unknown".
 
-    In host mode the container shares the host network namespace, so Docker
-    infrastructure interfaces (docker0, br-*, veth*) are visible.  In bridge
-    mode the container is isolated and only sees its own veth (named eth0).
+    This used to be its own copy of the diagnostic's first rule only, so it
+    called a macvlan container bridge (#3417). Shells out to ``ip``: call it
+    off the event loop.
     """
-    try:
-        import socket
+    from backend.app.services.discovery import detect_container_runtime
+    from backend.app.services.printer_diagnostic import _detect_container_network_mode
 
-        for _idx, name in socket.if_nameindex():
-            if name.startswith(("docker", "br-", "veth", "virbr")):
-                return "host"
+    try:
+        return _detect_container_network_mode(detect_container_runtime()) or "unknown"
     except Exception:
-        pass
-    return "bridge"
+        return "unknown"
 
 
 def _mask_subnet(subnet: str) -> str:
@@ -883,7 +882,7 @@ async def _collect_support_info() -> dict:
             info["docker"] = {
                 "container_memory_limit_bytes": mem_limit,
                 "container_memory_limit_formatted": _format_bytes(mem_limit) if mem_limit else None,
-                "network_mode_hint": _detect_docker_network_mode(),
+                "network_mode_hint": await asyncio.to_thread(_detect_docker_network_mode),
             }
         except Exception:
             logger.debug("Failed to collect Docker info", exc_info=True)

+ 75 - 8
backend/app/services/printer_diagnostic.py

@@ -11,6 +11,7 @@ user-side setup errors clustered on exactly these causes.
 
 import asyncio
 import ipaddress
+import json
 import logging
 import os
 import socket
@@ -253,8 +254,64 @@ def _has_native_interface() -> bool:
     return False
 
 
+# Link kinds that put a container straight onto the LAN with an address of
+# its own: the parent NIC's network, not a NAT behind the host.
+_LAN_LINK_KINDS = ("macvlan", "ipvlan")
+# The ``ip`` call reads the kernel's own table; a guard, not a budget.
+_IP_TIMEOUT = 2.0
+
+
+def _lan_link_kinds() -> dict[str, str] | None:
+    """The macvlan/ipvlan interfaces here: name -> "macvlan", "ipvlan" or
+    "ipvlan_l3" (L3 or L3S mode). None when the link kinds can't be read.
+
+    A macvlan or ipvlan interface is created in the host's namespace and
+    moved into the container's, so like a veth its ``iflink`` names an
+    interface elsewhere and ``_has_native_interface`` passes it by: such a
+    container read as bridge networking (#3417). Only the kernel's link kind
+    tells them apart, and only netlink carries it (``ip -d``): neither driver
+    gives sysfs a device type to read instead.
+    """
+    try:
+        result = subprocess.run(["ip", "-d", "-j", "link", "show"], capture_output=True, text=True, timeout=_IP_TIMEOUT)
+        if result.returncode != 0:
+            return None
+        found: dict[str, str] = {}
+        for link in json.loads(result.stdout or "[]"):
+            info = link.get("linkinfo") or {}
+            kind = info.get("info_kind")
+            if kind not in _LAN_LINK_KINDS or not link.get("ifname"):
+                continue
+            mode = (info.get("info_data") or {}).get("mode") if kind == "ipvlan" else None
+            # iproute2 prints l2/l3/l3s; L3 routes, so no broadcast or
+            # multicast crosses it. Any other mode is not known to.
+            found[link["ifname"]] = "ipvlan_l3" if mode in ("l3", "l3s") else kind
+        return found
+    except Exception:
+        logger.debug("ip -d link probe failed", exc_info=True)
+        return None
+
+
+def _lan_link_mode() -> str | None:
+    """How a macvlan or ipvlan interface here connects: ``macvlan``,
+    ``ipvlan`` (L2, or a mode not known to route), ``ipvlan_l3``; ``none``
+    when there is no such interface, None when it can't be told."""
+    kinds = _lan_link_kinds()
+    if kinds is None:
+        return None
+    values = set(kinds.values())
+    # A container on a bridge network and a macvlan one reaches the LAN
+    # through the macvlan; the most capable link answers.
+    for mode in ("macvlan", "ipvlan", "ipvlan_l3"):
+        if mode in values:
+            return mode
+    return "none"
+
+
 def _detect_container_network_mode(runtime: str | None) -> str | None:
-    """Return "host", "bridge", or None when it genuinely cannot be told.
+    """Return "host", "bridge", "macvlan", "ipvlan", "ipvlan_l3", or None
+    when it genuinely cannot be told. Reads the kernel's link table through
+    ``ip``: call it off the event loop.
 
     The first rule is the original Docker one and is kept exactly: a Docker
     *host* always has a docker0, so a container that can see it shares the
@@ -266,7 +323,8 @@ def _detect_container_network_mode(runtime: str | None) -> str | None:
     The second rule is the general form of the same idea and is what answers
     for Podman. The third is the fallback the first rule always implied: an
     OCI container that can see neither is isolated, which is what bridge
-    networking means.
+    networking means -- unless its interface is a macvlan or ipvlan, which
+    puts it on the LAN with an address of its own (#3417).
     """
     try:
         for _idx, name in socket.if_nameindex():
@@ -276,9 +334,11 @@ def _detect_container_network_mode(runtime: str | None) -> str | None:
         pass
     if _has_native_interface():
         return "host"
-    if runtime in OCI_RUNTIMES:
-        return "bridge"
-    return None
+    if runtime not in OCI_RUNTIMES:
+        return None
+    lan = _lan_link_mode()
+    # None without the link kinds: a veth and a macvlan look alike then.
+    return "bridge" if lan == "none" else lan
 
 
 def _host_source_ip(destination_ip: str) -> str | None:
@@ -512,17 +572,24 @@ async def run_connection_diagnostic(
             DiagnosticCheck(id="network_mode", status="skip", params={"reason": "system_container", "runtime": runtime})
         )
     else:
-        network_mode = _detect_container_network_mode(runtime)
+        # Off the loop: the link kinds come from `ip -d link show`.
+        network_mode = await asyncio.to_thread(_detect_container_network_mode, runtime)
         if network_mode is None:
             checks.append(
                 DiagnosticCheck(id="network_mode", status="skip", params={"reason": "unknown", "runtime": runtime})
             )
         else:
+            # macvlan and ipvlan L2 sit on the LAN with their own address, so
+            # discovery and the Virtual Printer work there. ipvlan L3 routes
+            # instead: no broadcast or multicast, so SSDP discovery can't.
+            params: dict = {"mode": network_mode, "runtime": runtime}
+            if network_mode in ("macvlan", "ipvlan", "ipvlan_l3"):
+                params["reason"] = network_mode
             checks.append(
                 DiagnosticCheck(
                     id="network_mode",
-                    status="pass" if network_mode == "host" else "warn",
-                    params={"mode": network_mode, "runtime": runtime},
+                    status="pass" if network_mode in ("host", "macvlan", "ipvlan") else "warn",
+                    params=params,
                 )
             )
 

+ 116 - 0
backend/tests/unit/services/test_container_lan_link_3417.py

@@ -0,0 +1,116 @@
+"""A macvlan or ipvlan container is on the LAN, not on bridge networking (#3417).
+
+Its interface is created in the host's namespace and moved into the
+container's, so like a veth its ``iflink`` names an interface elsewhere: the
+diagnostic called it bridge networking and told the user to recreate the
+container with host networking, though discovery and the Virtual Printer work.
+"""
+
+import json
+import subprocess
+from unittest.mock import patch
+
+import pytest
+
+from backend.app.services import discovery
+from backend.app.services.printer_diagnostic import (
+    _detect_container_network_mode,
+    _lan_link_kinds,
+)
+from backend.tests.unit.services.test_container_runtime_3092 import _BRIDGE_NETNS, _HOST_NETNS, MOD, _netns
+
+
+def _link(name, kind, mode=None):
+    info = {"info_kind": kind}
+    if mode:
+        info["info_data"] = {"mode": mode}
+    return {"ifname": name, "linkinfo": info}
+
+
+_MACVLAN = [_link("lo", None), _link("eth0", "macvlan")]
+
+
+class TestDetect:
+    def test_macvlan_is_on_the_lan(self):
+        with _netns(_BRIDGE_NETNS, _MACVLAN):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "macvlan"
+
+    def test_ipvlan_l2_is_on_the_lan(self):
+        with _netns(_BRIDGE_NETNS, [_link("eth0", "ipvlan", "l2")]):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "ipvlan"
+
+    @pytest.mark.parametrize("mode", ["l3", "l3s"])
+    def test_ipvlan_l3_routes(self, mode):
+        with _netns(_BRIDGE_NETNS, [_link("eth0", "ipvlan", mode)]):
+            assert _detect_container_network_mode(discovery.RUNTIME_PODMAN) == "ipvlan_l3"
+
+    def test_a_bridge_and_a_macvlan_network_reach_the_lan_through_the_macvlan(self):
+        interfaces = {**_BRIDGE_NETNS, "eth1": (3, 46, False)}
+        with _netns(interfaces, [_link("eth0", "veth"), _link("eth1", "macvlan")]):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "macvlan"
+
+    def test_an_ipvlan_mode_not_known_to_route_is_not_warned(self):
+        """iproute2 prints "unknown" for a mode it has no name for."""
+        with _netns(_BRIDGE_NETNS, [_link("eth0", "ipvlan", "unknown")]):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "ipvlan"
+
+    def test_ipvlan_wins_over_its_l3_sibling(self):
+        interfaces = {**_BRIDGE_NETNS, "eth1": (3, 46, False)}
+        with _netns(interfaces, [_link("eth0", "ipvlan", "l3"), _link("eth1", "ipvlan", "l2")]):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "ipvlan"
+
+    def test_a_veth_is_still_bridge(self):
+        with _netns(_BRIDGE_NETNS, [_link("eth0", "veth")]):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "bridge"
+
+    def test_host_networking_answers_first(self):
+        """A host may carry a macvlan shim to reach its containers."""
+        with _netns({**_HOST_NETNS, "shim": (3, 2, False)}, [_link("shim", "macvlan")]):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "host"
+
+    def test_no_engine_is_still_unknown(self):
+        with _netns(_BRIDGE_NETNS, _MACVLAN):
+            assert _detect_container_network_mode(None) is None
+
+
+class TestUnreadableLinks:
+    """Without the link kinds a veth and a macvlan look alike: say so."""
+
+    @pytest.mark.parametrize(
+        "ip",
+        [
+            FileNotFoundError("ip"),
+            subprocess.CompletedProcess([], 1, stdout="", stderr="no netlink"),
+            subprocess.CompletedProcess([], 0, stdout="not json", stderr=""),
+        ],
+    )
+    def test_the_mode_is_unknown_rather_than_bridge(self, ip):
+        with _netns(_BRIDGE_NETNS):
+            kwargs = {"side_effect": ip} if isinstance(ip, Exception) else {"return_value": ip}
+            with patch(f"{MOD}.subprocess.run", **kwargs):
+                assert _lan_link_kinds() is None
+                assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) is None
+
+    def test_host_networking_needs_no_link_kinds(self):
+        with _netns(_HOST_NETNS), patch(f"{MOD}.subprocess.run", side_effect=FileNotFoundError("ip")):
+            assert _detect_container_network_mode(discovery.RUNTIME_DOCKER) == "host"
+
+
+class TestSupportPackage:
+    def test_the_support_package_reads_the_mode_as_the_diagnostic_does(self):
+        from backend.app.api.routes.support import _detect_docker_network_mode
+
+        with (
+            _netns(_BRIDGE_NETNS, _MACVLAN),
+            patch("backend.app.services.discovery.detect_container_runtime", return_value=discovery.RUNTIME_DOCKER),
+        ):
+            assert _detect_docker_network_mode() == "macvlan"
+
+    def test_podman_host_networking_is_no_longer_bridge_there(self):
+        from backend.app.api.routes.support import _detect_docker_network_mode
+
+        with (
+            _netns(_HOST_NETNS),
+            patch("backend.app.services.discovery.detect_container_runtime", return_value=discovery.RUNTIME_PODMAN),
+        ):
+            assert _detect_docker_network_mode() == "host"

+ 7 - 2
backend/tests/unit/services/test_container_runtime_3092.py

@@ -9,7 +9,9 @@ its network namespace is the host's.
 
 import builtins
 import io
+import json
 import os
+import subprocess
 from contextlib import contextmanager
 from unittest.mock import patch
 
@@ -173,9 +175,12 @@ def _sysfs(interfaces):
 
 
 @contextmanager
-def _netns(interfaces):
+def _netns(interfaces, links=None):
+    """``links`` is what ``ip -d -j link show`` reports; none of the
+    interfaces is a macvlan or ipvlan by default (#3417)."""
     path_patch, names_patch = _sysfs(interfaces)
-    with path_patch, names_patch:
+    ip = subprocess.CompletedProcess([], 0, stdout=json.dumps(links or []), stderr="")
+    with path_patch, names_patch, patch(f"{MOD}.subprocess.run", return_value=ip):
         yield
 
 

+ 24 - 0
backend/tests/unit/services/test_printer_diagnostic.py

@@ -12,6 +12,8 @@ import types
 from contextlib import ExitStack
 from unittest.mock import AsyncMock, MagicMock, patch
 
+import pytest
+
 from backend.app.services.printer_diagnostic import (
     _check_ftps_tls,
     _host_source_ip,
@@ -367,6 +369,28 @@ class TestExistingPrinter:
             result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
         assert _statuses(result)["network_mode"] == "warn"
 
+    @pytest.mark.parametrize("mode", ["macvlan", "ipvlan"])
+    async def test_a_container_on_the_lan_passes_and_checks_its_subnet(self, mode):
+        """#3417: its own LAN address, so discovery and the Virtual Printer work."""
+        with _Env(network_mode=mode, state=_state()):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        check = _check(result, "network_mode")
+        assert check.status == "pass"
+        assert check.params["reason"] == mode
+        assert _statuses(result)["subnet"] == "pass"
+
+    async def test_ipvlan_l3_warns_about_discovery(self):
+        with _Env(network_mode="ipvlan_l3", state=_state()):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        check = _check(result, "network_mode")
+        assert check.status == "warn"
+        assert check.params["reason"] == "ipvlan_l3"
+
+    async def test_bridge_mode_carries_no_variant(self):
+        with _Env(network_mode="bridge", state=_state()):
+            result = await run_connection_diagnostic("192.168.1.50", printer=_printer())
+        assert "reason" not in _check(result, "network_mode").params
+
     async def test_undetectable_mode_skips_rather_than_guessing(self):
         """A container we cannot read must not be told to recreate itself."""
         with _Env(runtime="Podman", network_mode=None, state=_state()):

+ 34 - 0
frontend/src/__tests__/components/ConnectionDiagnosticModal.test.tsx

@@ -245,6 +245,40 @@ describe('ConnectionDiagnosticModal', () => {
     spy.mockRestore();
   });
 
+  it('passes a macvlan container without offering host networking (#3417)', async () => {
+    const spy = vi.spyOn(api, 'diagnosePrinter').mockResolvedValue({
+      ...PROBLEM_RESULT,
+      overall: 'ok',
+      checks: [{ id: 'network_mode', status: 'pass', params: { mode: 'macvlan', reason: 'macvlan', runtime: 'Docker' } }],
+    });
+
+    renderModal({ printerId: 1, printerName: 'Test P1S', onClose: vi.fn() });
+
+    await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
+    expect(await screen.findByText(/Running in Docker on a macvlan network/i)).toBeInTheDocument();
+    expect(screen.queryByText(/host networking/i)).not.toBeInTheDocument();
+
+    spy.mockRestore();
+  });
+
+  it('warns that ipvlan L3 hides the Virtual Printer from the slicer (#3417)', async () => {
+    const spy = vi.spyOn(api, 'diagnosePrinter').mockResolvedValue({
+      ...PROBLEM_RESULT,
+      overall: 'warnings',
+      checks: [
+        { id: 'network_mode', status: 'warn', params: { mode: 'ipvlan_l3', reason: 'ipvlan_l3', runtime: 'Docker' } },
+      ],
+    });
+
+    renderModal({ printerId: 1, printerName: 'Test P1S', onClose: vi.fn() });
+
+    await waitFor(() => expect(spy).toHaveBeenCalledTimes(1));
+    expect(await screen.findByText(/slicer cannot find the Virtual Printer/i)).toBeInTheDocument();
+    expect(screen.queryByText(/bridge networking/i)).not.toBeInTheDocument();
+
+    spy.mockRestore();
+  });
+
   it('names the interpreter when macOS has no signature to grant against (#3114)', async () => {
     const spy = vi.spyOn(api, 'diagnosePrinter').mockResolvedValue({
       ...PROBLEM_RESULT,

+ 3 - 0
frontend/src/i18n/locales/de.ts

@@ -7645,6 +7645,9 @@ export default {
         skip: 'Läuft nicht in einem Container — nicht zutreffend.',
         skip_unknown: 'Bambuddy läuft in {{runtime}}, der Netzwerkmodus konnte jedoch nicht ermittelt werden. Falls die Druckererkennung oder der virtuelle Drucker nicht funktionieren, erstellen Sie den Container mit Host-Netzwerk neu.',
         skip_system_container: 'Bambuddy läuft in einem {{runtime}}-Systemcontainer, der wie eine virtuelle Maschine direkt im LAN liegt — nicht zutreffend.',
+        pass_macvlan: 'Läuft in {{runtime}} in einem macvlan-Netzwerk, mit eigener Adresse im LAN.',
+        pass_ipvlan: 'Läuft in {{runtime}} in einem ipvlan-Netzwerk (L2), mit eigener Adresse im LAN.',
+        warn_ipvlan_l3: 'Bambuddy läuft in {{runtime}} in einem ipvlan-Netzwerk im L3-Modus. Der L3-Modus leitet weder Broadcast noch Multicast weiter, daher findet der Slicer den virtuellen Drucker nicht und Drucker lassen sich nur per Subnetz-Scan finden — verwenden Sie stattdessen ein macvlan-Netzwerk oder den ipvlan-L2-Modus.',
       },
       subnet: {
         title: 'Netzwerk-Subnetz',

+ 3 - 0
frontend/src/i18n/locales/en.ts

@@ -7698,6 +7698,9 @@ export default {
         skip: 'Not running in a container — not applicable.',
         skip_unknown: 'Bambuddy is running in {{runtime}}, but its network mode could not be determined. If printer discovery or the Virtual Printer do not work, recreate the container with host networking.',
         skip_system_container: 'Bambuddy is running in a {{runtime}} system container, which sits on the LAN like a virtual machine — not applicable.',
+        pass_macvlan: 'Running in {{runtime}} on a macvlan network, with its own address on the LAN.',
+        pass_ipvlan: 'Running in {{runtime}} on an ipvlan network (L2), with its own address on the LAN.',
+        warn_ipvlan_l3: 'Bambuddy is running in {{runtime}} on an ipvlan network in L3 mode. L3 mode passes no broadcast or multicast, so the slicer cannot find the Virtual Printer and printers can only be found with the subnet scan — use a macvlan network or ipvlan L2 mode instead.',
       },
       subnet: {
         title: 'Network subnet',

+ 3 - 0
frontend/src/i18n/locales/es.ts

@@ -7653,6 +7653,9 @@ export default {
         skip: 'No se está ejecutando en un contenedor — no aplicable.',
         skip_unknown: 'Bambuddy se está ejecutando en {{runtime}}, pero no se pudo determinar su modo de red. Si la detección de impresoras o la impresora virtual no funcionan, vuelva a crear el contenedor con red de host.',
         skip_system_container: 'Bambuddy se está ejecutando en un contenedor de sistema {{runtime}}, que está en la LAN como una máquina virtual — no aplicable.',
+        pass_macvlan: 'Ejecutándose en {{runtime}} en una red macvlan, con su propia dirección en la LAN.',
+        pass_ipvlan: 'Ejecutándose en {{runtime}} en una red ipvlan (L2), con su propia dirección en la LAN.',
+        warn_ipvlan_l3: 'Bambuddy se está ejecutando en {{runtime}} en una red ipvlan en modo L3. El modo L3 no transmite broadcast ni multicast, por lo que el slicer no encuentra la impresora virtual y las impresoras solo se pueden encontrar con el escaneo de subred — usa una red macvlan o el modo ipvlan L2.',
       },
       subnet: {
         title: 'Subred de la red',

+ 3 - 0
frontend/src/i18n/locales/fr.ts

@@ -7633,6 +7633,9 @@ export default {
         skip: 'Ne fonctionne pas dans un conteneur — non applicable.',
         skip_unknown: 'Bambuddy fonctionne dans {{runtime}}, mais son mode réseau n\'a pas pu être déterminé. Si la découverte d\'imprimantes ou l\'imprimante virtuelle ne fonctionnent pas, recréez le conteneur en réseau host.',
         skip_system_container: 'Bambuddy fonctionne dans un conteneur système {{runtime}}, présent sur le LAN comme une machine virtuelle — non applicable.',
+        pass_macvlan: 'Fonctionne dans {{runtime}} sur un réseau macvlan, avec sa propre adresse sur le LAN.',
+        pass_ipvlan: 'Fonctionne dans {{runtime}} sur un réseau ipvlan (L2), avec sa propre adresse sur le LAN.',
+        warn_ipvlan_l3: "Bambuddy fonctionne dans {{runtime}} sur un réseau ipvlan en mode L3. Le mode L3 ne transmet ni broadcast ni multicast : le slicer ne trouve pas l'imprimante virtuelle et les imprimantes ne peuvent être trouvées que par l'analyse du sous-réseau — utilisez plutôt un réseau macvlan ou le mode ipvlan L2.",
       },
       subnet: {
         title: 'Sous-réseau',

+ 3 - 0
frontend/src/i18n/locales/it.ts

@@ -7632,6 +7632,9 @@ export default {
         skip: 'Non in esecuzione in un container — non applicabile.',
         skip_unknown: 'Bambuddy è in esecuzione in {{runtime}}, ma non è stato possibile determinarne la modalità di rete. Se il rilevamento delle stampanti o la stampante virtuale non funzionano, ricrea il container con rete host.',
         skip_system_container: 'Bambuddy è in esecuzione in un container di sistema {{runtime}}, presente sulla LAN come una macchina virtuale — non applicabile.',
+        pass_macvlan: 'In esecuzione in {{runtime}} su una rete macvlan, con un proprio indirizzo sulla LAN.',
+        pass_ipvlan: 'In esecuzione in {{runtime}} su una rete ipvlan (L2), con un proprio indirizzo sulla LAN.',
+        warn_ipvlan_l3: 'Bambuddy è in esecuzione in {{runtime}} su una rete ipvlan in modalità L3. La modalità L3 non inoltra broadcast né multicast, quindi lo slicer non trova la stampante virtuale e le stampanti si possono trovare solo con la scansione della sottorete — usa invece una rete macvlan o la modalità ipvlan L2.',
       },
       subnet: {
         title: 'Sottorete',

+ 3 - 0
frontend/src/i18n/locales/ja.ts

@@ -7646,6 +7646,9 @@ export default {
         skip: 'コンテナで実行されていません — 該当しません。',
         skip_unknown: 'Bambuddy は {{runtime}} で実行されていますが、ネットワークモードを判別できませんでした。プリンター検出や仮想プリンターが動作しない場合は、ホストネットワークでコンテナを再作成してください。',
         skip_system_container: 'Bambuddy は {{runtime}} のシステムコンテナで実行されており、仮想マシンと同様に LAN 上にあります — 該当しません。',
+        pass_macvlan: '{{runtime}} の macvlan ネットワークで実行中です。LAN 上に独自のアドレスを持っています。',
+        pass_ipvlan: '{{runtime}} の ipvlan ネットワーク (L2) で実行中です。LAN 上に独自のアドレスを持っています。',
+        warn_ipvlan_l3: 'Bambuddy は {{runtime}} の L3 モードの ipvlan ネットワークで実行されています。L3 モードはブロードキャストもマルチキャストも通さないため、スライサーは仮想プリンターを見つけられず、プリンターはサブネットスキャンでしか見つかりません — 代わりに macvlan ネットワークか ipvlan の L2 モードを使用してください。',
       },
       subnet: {
         title: 'ネットワークサブネット',

+ 4 - 1
frontend/src/i18n/locales/ko.ts

@@ -7885,7 +7885,10 @@ export default {
         warn: 'Bambuddy가 {{runtime}}에서 브리지 네트워크로 실행 중입니다. 프린터 검색과 가상 프린터에는 호스트 네트워크가 필요합니다 — 호스트 네트워크로 컨테이너를 재생성하세요(docker-compose에서는 "network_mode: host", Podman에서는 "--network=host").',
         skip: '컨테이너에서 실행 중이 아닙니다 — 해당 없음.',
         skip_unknown: 'Bambuddy가 {{runtime}}에서 실행 중이지만 네트워크 모드를 확인할 수 없습니다. 프린터 검색이나 가상 프린터가 작동하지 않으면 호스트 네트워크로 컨테이너를 재생성하세요.',
-        skip_system_container: 'Bambuddy가 {{runtime}} 시스템 컨테이너에서 실행 중이며, 가상 머신처럼 LAN에 연결되어 있습니다 — 해당 없음.'
+        skip_system_container: 'Bambuddy가 {{runtime}} 시스템 컨테이너에서 실행 중이며, 가상 머신처럼 LAN에 연결되어 있습니다 — 해당 없음.',
+        pass_macvlan: '{{runtime}}의 macvlan 네트워크에서 실행 중이며, LAN에 자체 주소가 있습니다.',
+        pass_ipvlan: '{{runtime}}의 ipvlan 네트워크(L2)에서 실행 중이며, LAN에 자체 주소가 있습니다.',
+        warn_ipvlan_l3: 'Bambuddy가 {{runtime}}의 L3 모드 ipvlan 네트워크에서 실행 중입니다. L3 모드는 브로드캐스트와 멀티캐스트를 전달하지 않으므로 슬라이서가 가상 프린터를 찾을 수 없고 프린터는 서브넷 스캔으로만 찾을 수 있습니다 — 대신 macvlan 네트워크 또는 ipvlan L2 모드를 사용하세요.'
       },
       subnet: {
         title: '네트워크 서브넷',

+ 3 - 0
frontend/src/i18n/locales/nl.ts

@@ -7696,6 +7696,9 @@ export default {
         skip: 'Draait niet in een container — niet van toepassing.',
         skip_unknown: 'Bambuddy draait in {{runtime}}, maar de netwerkmodus kon niet worden bepaald. Als printerdetectie of de Virtuele printer niet werken, maak de container dan opnieuw aan met host-netwerk.',
         skip_system_container: 'Bambuddy draait in een {{runtime}}-systeemcontainer, die net als een virtuele machine op het LAN zit — niet van toepassing.',
+        pass_macvlan: 'Draait in {{runtime}} op een macvlan-netwerk, met een eigen adres op het LAN.',
+        pass_ipvlan: 'Draait in {{runtime}} op een ipvlan-netwerk (L2), met een eigen adres op het LAN.',
+        warn_ipvlan_l3: 'Bambuddy draait in {{runtime}} op een ipvlan-netwerk in L3-modus. L3-modus laat geen broadcast of multicast door, dus de slicer vindt de virtuele printer niet en printers zijn alleen met de subnetscan te vinden — gebruik in plaats daarvan een macvlan-netwerk of ipvlan L2-modus.',
       },
       subnet: {
         title: 'Netwerksubnet',

+ 3 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -7632,6 +7632,9 @@ export default {
         skip: 'Não está sendo executado em um contêiner — não aplicável.',
         skip_unknown: 'O Bambuddy está sendo executado em {{runtime}}, mas não foi possível determinar o modo de rede. Se a descoberta de impressoras ou a impressora virtual não funcionarem, recrie o contêiner com rede host.',
         skip_system_container: 'O Bambuddy está sendo executado em um contêiner de sistema {{runtime}}, que fica na LAN como uma máquina virtual — não aplicável.',
+        pass_macvlan: 'Executando em {{runtime}} em uma rede macvlan, com endereço próprio na LAN.',
+        pass_ipvlan: 'Executando em {{runtime}} em uma rede ipvlan (L2), com endereço próprio na LAN.',
+        warn_ipvlan_l3: 'O Bambuddy está sendo executado em {{runtime}} em uma rede ipvlan no modo L3. O modo L3 não repassa broadcast nem multicast, então o fatiador não encontra a impressora virtual e as impressoras só podem ser encontradas pela varredura de sub-rede — use uma rede macvlan ou o modo ipvlan L2.',
       },
       subnet: {
         title: 'Sub-rede',

+ 3 - 0
frontend/src/i18n/locales/ru.ts

@@ -7275,6 +7275,9 @@ export default {
         skip: "Bambuddy запущен не в контейнере — проверка неприменима.",
         skip_unknown: "Bambuddy работает в {{runtime}}, но определить сетевой режим не удалось. Если обнаружение принтеров или виртуальный принтер не работают, пересоздайте контейнер с сетью host.",
         skip_system_container: "Bambuddy работает в системном контейнере {{runtime}}, который находится в локальной сети как виртуальная машина — проверка неприменима.",
+        pass_macvlan: "Работает в {{runtime}} в сети macvlan, с собственным адресом в локальной сети.",
+        pass_ipvlan: "Работает в {{runtime}} в сети ipvlan (L2), с собственным адресом в локальной сети.",
+        warn_ipvlan_l3: "Bambuddy работает в {{runtime}} в сети ipvlan в режиме L3. Режим L3 не пропускает широковещательные и многоадресные пакеты, поэтому слайсер не найдёт виртуальный принтер, а принтеры можно найти только сканированием подсети — используйте сеть macvlan или режим ipvlan L2.",
       },
       subnet: {
         title: "Подсеть",

+ 3 - 0
frontend/src/i18n/locales/sv.ts

@@ -7694,6 +7694,9 @@ errors: {
         skip: 'Körs inte i Docker — inte tillämpligt.',
         skip_unknown: 'Bambuddy körs i {{runtime}}, men dess nätverksläge kunde inte fastställas. Om skrivarupptäckt eller den virtuella skrivaren inte fungerar, återskapa containern med värdnätverk.',
         skip_system_container: 'Bambuddy körs i en {{runtime}} systemcontainer, som sitter på LAN som en virtuell maskin — inte tillämpligt.',
+        pass_macvlan: 'Körs i {{runtime}} på ett macvlan-nätverk, med en egen adress på LAN.',
+        pass_ipvlan: 'Körs i {{runtime}} på ett ipvlan-nätverk (L2), med en egen adress på LAN.',
+        warn_ipvlan_l3: 'Bambuddy körs i {{runtime}} på ett ipvlan-nätverk i L3-läge. L3-läget släpper inte igenom broadcast eller multicast, så slicern hittar inte den virtuella skrivaren och skrivare kan bara hittas med nätverksskanningen — använd ett macvlan-nätverk eller ipvlan L2-läge i stället.',
       },
       subnet: {
         title: 'Nätverkssubnät',

+ 3 - 0
frontend/src/i18n/locales/tr.ts

@@ -7584,6 +7584,9 @@ export default {
         skip: 'Bir konteyner içinde çalışmıyor — uygulanamaz.',
         skip_unknown: 'Bambuddy {{runtime}} içinde çalışıyor ancak ağ modu belirlenemedi. Yazıcı keşfi veya Sanal Yazıcı çalışmıyorsa konteyneri ana bilgisayar ağıyla yeniden oluşturun.',
         skip_system_container: 'Bambuddy, sanal makine gibi doğrudan yerel ağda bulunan bir {{runtime}} sistem konteyneri içinde çalışıyor — uygulanamaz.',
+        pass_macvlan: '{{runtime}} içinde macvlan ağında, yerel ağda kendi adresiyle çalışıyor.',
+        pass_ipvlan: '{{runtime}} içinde ipvlan ağında (L2), yerel ağda kendi adresiyle çalışıyor.',
+        warn_ipvlan_l3: 'Bambuddy, {{runtime}} içinde L3 modundaki bir ipvlan ağında çalışıyor. L3 modu yayın ve çoklu yayın paketlerini iletmez, bu nedenle dilimleyici sanal yazıcıyı bulamaz ve yazıcılar yalnızca alt ağ taramasıyla bulunabilir — bunun yerine bir macvlan ağı veya ipvlan L2 modunu kullanın.',
       },
       subnet: {
         title: 'Ağ alt ağı',

+ 3 - 0
frontend/src/i18n/locales/uk.ts

@@ -7686,6 +7686,9 @@ export default {
         skip: "Не працює в контейнері — не застосовується.",
         skip_unknown: "Bambuddy працює в {{runtime}}, але визначити мережевий режим не вдалося. Якщо виявлення принтерів або віртуальний принтер не працюють, повторно створіть контейнер із мережею хоста.",
         skip_system_container: "Bambuddy працює в системному контейнері {{runtime}}, який перебуває в локальній мережі як віртуальна машина — не застосовується.",
+        pass_macvlan: "Працює в {{runtime}} у мережі macvlan, із власною адресою в локальній мережі.",
+        pass_ipvlan: "Працює в {{runtime}} у мережі ipvlan (L2), із власною адресою в локальній мережі.",
+        warn_ipvlan_l3: "Bambuddy працює в {{runtime}} у мережі ipvlan у режимі L3. Режим L3 не пропускає широкомовні та багатоадресні пакети, тому слайсер не знайде віртуальний принтер, а принтери можна знайти лише скануванням підмережі — використовуйте мережу macvlan або режим ipvlan L2.",
       },
       subnet: {
         title: "Мережева підмережа",

+ 3 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -7630,6 +7630,9 @@ export default {
         skip: '未在容器中运行 — 不适用。',
         skip_unknown: 'Bambuddy 正在 {{runtime}} 中运行,但无法确定其网络模式。如果打印机发现或虚拟打印机无法使用,请使用 host 网络重新创建容器。',
         skip_system_container: 'Bambuddy 正在 {{runtime}} 系统容器中运行,它像虚拟机一样直接位于局域网中 — 不适用。',
+        pass_macvlan: '正在 {{runtime}} 的 macvlan 网络中运行,在局域网中拥有自己的地址。',
+        pass_ipvlan: '正在 {{runtime}} 的 ipvlan 网络 (L2) 中运行,在局域网中拥有自己的地址。',
+        warn_ipvlan_l3: 'Bambuddy 正在 {{runtime}} 的 L3 模式 ipvlan 网络中运行。L3 模式不转发广播和组播,因此切片软件无法找到虚拟打印机,打印机也只能通过子网扫描找到 — 请改用 macvlan 网络或 ipvlan L2 模式。',
       },
       subnet: {
         title: '网络子网',

+ 3 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -7630,6 +7630,9 @@ export default {
         skip: '未在容器中執行 — 不適用。',
         skip_unknown: 'Bambuddy 正在 {{runtime}} 中執行,但無法判斷其網路模式。如果印表機探索或虛擬印表機無法使用,請使用 host 網路重新建立容器。',
         skip_system_container: 'Bambuddy 正在 {{runtime}} 系統容器中執行,它像虛擬機器一樣直接位於區域網路中 — 不適用。',
+        pass_macvlan: '正在 {{runtime}} 的 macvlan 網路中執行,在區域網路中擁有自己的位址。',
+        pass_ipvlan: '正在 {{runtime}} 的 ipvlan 網路 (L2) 中執行,在區域網路中擁有自己的位址。',
+        warn_ipvlan_l3: 'Bambuddy 正在 {{runtime}} 的 L3 模式 ipvlan 網路中執行。L3 模式不轉送廣播與多播,因此切片軟體無法找到虛擬印表機,印表機也只能透過子網路掃描找到 — 請改用 macvlan 網路或 ipvlan L2 模式。',
       },
       subnet: {
         title: '網路子網路',