---
title: Quality of Service (QoS)
description: Sort, prioritize, and shape TVPN traffic across multiple network links using a traffic policy.
url: "https://docs.somewear.app/networking/qos/"
updated: "2026-10-09"
requires: {}
facets:
  hardware: []
  client:
    - tvpn:
        since: baseline
  satcom: []
---

> **Note**
>
> Traffic classification and link pinning work on every TVPN endpoint. Per-link bandwidth shaping and priority scheduling under congestion run on Linux endpoints. See [Operating system support](https://docs.somewear.app/networking/qos/#operating-system-support).

Quality-of-Service (QoS) is configured using a separate traffic policy file. It lets you:

- Sort VPN traffic into classes
- Prioritize some traffic classes over others
- Choose which network interfaces each class can use (or should avoid)
- Shape bandwidth per network interface

## Example

The policy below uses two network interfaces and gives MAVLink C2 traffic higher priority than a video stream. The video runs only over Starlink; MAVLink can use both links.

Enable traffic policy QoS in `config.toml`:

```toml
[traffic-policy]
enabled = true

# Optional. Defaults to traffic-policy.toml in the same directory as config.toml.
# config-path = "/path/to/traffic-policy.toml"
```

Then write the policy in `traffic-policy.toml`. TVPN handles each outgoing packet in three steps: a rule assigns it to a class, the class sets its priority and the links it may use, and each link names a network interface. The three parts below follow that order. All three go in the same file, in any order.

**Use rules to assign a class to each packet.**

TVPN checks rules in `priority` order: lower numbers are checked first, the first match assigns the packet to its `class`.

```toml
[[rule]]
priority = 10
match    = { proto = "udp", dst-port = "14550" }
class    = "mavlink"

[[rule]]
priority = 20
match    = { proto = "udp", dst-port = "5004-5005" }
class    = "video"
```

**Classes set priority and links.**

A class’s `priority` (`critical`, `important`, `normal`, or `bulk`) sets how its traffic is treated. MAVLink outranks video because its class is `critical` and video’s is `important`. `pin-links` keeps video on Starlink. Unmatched packets fall into `default`.

```toml
[class.mavlink]
priority = "critical"

[class.video]
priority  = "important"
pin-links = ["starlink"]

[class.default]
priority = "normal"
```

**Links name the interfaces.**

Each `[link.NAME]` maps a name used in `pin-links` to an interface. Optional upstream bandwidth and round-trip time let Linux endpoints shape and queue traffic. MAVLink has no `pin-links`, so it uses both links.

```toml
[link.lte]
iface         = "eth0"
bandwidth-bps = 5_000_000    # ~5 Mbit/s upstream
rtt-ms        = 75

[link.starlink]
iface         = "eth1"
bandwidth-bps = 15_000_000   # ~15 Mbit/s upstream
rtt-ms        = 40
```

## Configuration options

### Global options

These belong in the **`[qos]`** section of `traffic-policy.toml`:

| Field                    | Type    | Default    | Notes                                                                                                                                                                                                                                                                                             |
| ------------------------ | ------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queue-management`       | string  | `"active"` | Controls whether TVPN configures Linux traffic control (`tc`) on your interfaces. `"active"`: TVPN manages qdisc settings automatically. `"passive"`: you manage `tc` yourself, or have a custom `tc` configuration. Linux only; requires `iproute2`.                                             |
| `passthrough-inner-dscp` | boolean | `false`    | When true, mirrors the inner packet’s DSCP value onto the outer UDP VPN transport packet, so local network infrastructure can act on it. The original packet’s DSCP is preserved either way. Only relevant if your application already marks its own packets with DSCP. Not supported on Windows. |
| `wash-dscp-egress`       | boolean | `true`     | When true, the DSCP field is scrubbed back to the default value before the packet leaves the network interface, preventing internal priority signaling from leaking onto the physical network. Set to false to preserve DSCP on the wire. Linux only.                                             |

```toml
[qos]
queue-management       = "active"   # "active" | "passive" (default "active")
passthrough-inner-dscp = false
wash-dscp-egress       = true
```

### Links

The **`[link.NAME]`** section name is a label of your choosing; classes reference it via `pin-links`/`avoid-links`.

| Field           | Type               | Required | Default  | Notes                                                                                                                                                                  |
| --------------- | ------------------ | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `iface`         | string             | yes      | —        | OS-level interface name. Linux/macOS: `eth0`, `wlan0`, `wwan0`. Windows: friendly name (e.g. “Ethernet”).                                                              |
| `bandwidth-bps` | integer (bits/sec) | no       | unshaped | Egress rate cap. Underscores allowed (`5_000_000` = 5 Mbit/s). Omit for variable-rate or unknown links.                                                                |
| `rtt-ms`        | integer (ms)       | no       | 100 ms   | Average round-trip time. Used by active queue management to tune when to drop older packets. Typical: 1 (LAN), 40 (Starlink), 75 (LTE), 700 (geostationary satellite). |

### Classes

The **`[class.NAME]`** section name is a label of your choosing, with one special case: a class named exactly `default` becomes the catch-all bucket for any packet that doesn’t match a rule. Without `[class.default]`, unmatched packets pass through unclassified.

| Field         | Type            | Required | Default                    | Notes                                                                                                                                                                                              |
| ------------- | --------------- | -------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `priority`    | string          | yes      | —                          | One of `critical`, `important`, `normal`, `bulk`. See the priority values table below.                                                                                                             |
| `pin-links`   | list of strings | no       | empty (all links eligible) | If set, traffic in this class only egresses through these links, e.g. `pin-links = ["lte", "starlink"]`. Mutually exclusive with `avoid-links`. Each entry must reference a defined `[link.NAME]`. |
| `avoid-links` | list of strings | no       | empty                      | If set, traffic avoids these links, e.g. `avoid-links = ["lte"]`. Mutually exclusive with `pin-links`.                                                                                             |

#### Priority values

| Value       | Description                                                    |
| ----------- | -------------------------------------------------------------- |
| `critical`  | Small, urgent traffic that must get through under congestion.  |
| `important` | Steady, latency-sensitive flows needing consistent throughput. |
| `normal`    | Default best-effort traffic with no special urgency.           |
| `bulk`      | Background traffic that yields to everything else.             |

> **Caution**
>
> **Keep bulk transfers out of `critical`.** The critical class is sized for small, urgent flows. Classifying a file transfer or a saturating test stream as `critical` puts it in direct competition with the command-and-control traffic the class exists to protect, and both suffer. Give steady high-rate flows `important`, and background transfers `bulk`.

### Rules

Each rule is declared with `[[rule]]` — double brackets.

| Field      | Type         | Required | Default | Notes                                                                                                                                                                                                                                                                                                  |
| ---------- | ------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `priority` | integer      | yes      | —       | Evaluation order, `0` to `4_294_967_295`. Lower numbers are checked first; the first match wins. When rules can match the same packet, give the more specific rule the lower number. A matched packet’s treatment comes from its class’s `priority`, not this number. Must be unique across all rules. |
| `match`    | inline table | yes      | —       | Match criteria. All specified subfields must match (logical AND). Any subfield you omit is treated as a wildcard. See the match subfields table below.                                                                                                                                                 |
| `class`    | string       | yes      | —       | Name of a defined `[class.NAME]` to assign matched packets to.                                                                                                                                                                                                                                         |

#### Rule match subfields

| Field      | Type            | Range / Examples                   | Notes                                                                                                                                                                                   |
| ---------- | --------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `proto`    | string (enum)   | `"tcp"`, `"udp"`, `"icmp"`         | Transport protocol. ICMP rules can’t also set `src-port` or `dst-port`.                                                                                                                 |
| `src-ip`   | string          | `"10.0.0.5"`, `"10.0.0.0/24"`      | VPN/overlay IPv4 address or CIDR of the source endpoint (the inner packet’s source).                                                                                                    |
| `dst-ip`   | string          | `"192.168.1.1"`, `"172.16.0.0/12"` | VPN/overlay IPv4 address or CIDR of the destination endpoint (the inner packet’s destination).                                                                                          |
| `src-port` | string          | `"14550"` or `"5004-5005"`         | Single port or inclusive range. Valid ports `1`–`65535`.                                                                                                                                |
| `dst-port` | string          | same as `src-port`                 | Single port or inclusive range. Valid ports `1`–`65535`.                                                                                                                                |
| `dscp`     | list of strings | `["CS6"]`, `["AF41", "EF"]`        | DSCP code-point names matched on the inner packet. Any value in the list matches (OR). Accepts the full IETF vocabulary: `CS0`–`CS7`, `AF11`–`AF43`, `EF`, `VA`, `LE`, `BE`, `DEFAULT`. |

#### More example rules

```toml
# Classify important TCP traffic
[class.tak]
priority = "important"

[[rule]]
priority = 30
# Without proto = "tcp", the rule would also match UDP/8087
match    = { proto = "tcp", dst-port = "8087" }
class    = "tak"

# Deprioritize certain traffic
[class.uploads]
priority = "bulk"

[[rule]]
priority = 40
match    = { proto = "tcp", dst-ip = "100.67.0.0/24", dst-port = "443" }
class    = "uploads"

# Consider ping traffic at the highest priority level
[class.health-checks]
priority = "critical"

[[rule]]
priority = 50
match    = { proto = "icmp" }
class    = "health-checks"

# Remember: Each rule's priority must be unique across the whole policy.
```

## Operating system support

| Capability                                             | Linux | macOS | FreeBSD | Windows |
| ------------------------------------------------------ | ----- | ----- | ------- | ------- |
| Classification (rule engine)                           | ✓     | ✓     | ✓       | ✓       |
| Class-based link pinning (`pin-links` / `avoid-links`) | ✓     | ✓     | ✓       | ✓       |
| Per-class DSCP marking on the wire                     | ✓     | ✓     | ✓       | ✗       |
| Pass-through inner DSCP (`passthrough-inner-dscp`)     | ✓     | ✓     | ✓       | ✗       |
| Per-link bandwidth shaping (`bandwidth-bps`)           | ✓     | ✗     | ✗       | ✗       |
| Priority-based scheduling under congestion             | ✓     | ✗     | ✗       | ✗       |

## Troubleshooting

**Validate before deploying:**

```bash
tvpn traffic-policy validate /var/lib/somewear/tvpn/traffic-policy.toml
```

This parses and validates the file without touching the running daemon, and reports every error it finds.

To print a starter policy pre-filled with this host’s interfaces:

```bash
tvpn traffic-policy generate
```

**Check what actually loaded:**

```bash
tvpn traffic-policy show
```

TVPN starts normally even if your QoS policy has errors — only QoS is affected, the rest of the daemon comes up. This command shows the classes and rules TVPN actually loaded, the status of each link, and whether the `tc` qdisc installed successfully on Linux.

**Log locations** — validation errors are written to the daemon log at startup:

| OS           | Path                                       |
| ------------ | ------------------------------------------ |
| Linux, macOS | `/var/lib/somewear/tvpn/log/tvpn.log`      |
| FreeBSD      | `/var/log/somewear/tvpn/tvpn.log`          |
| Windows      | `%ProgramData%\Somewear\tvpn\log\tvpn.log` |

Common validation failures to look for in the log:

- Two `[[rule]]` blocks with the same priority value (priorities must be unique)
- A rule referencing a class name that isn’t defined
- A class referencing a link name that isn’t defined
- A class with both `pin-links` and `avoid-links` set
- `proto = "icmp"` combined with `src-port` or `dst-port` (ICMP has no ports)
- A typo in a field name — unknown keys are rejected outright

**Qdisc installation failures** — on Linux, if `tvpn traffic-policy show` reports `failed: ...` for a link, check that `iproute2` is installed and your kernel has CAKE support:

```bash
modinfo sch_cake
```

If your platform’s kernel doesn’t provide CAKE and can’t be changed, set `queue-management = "passive"`. Classification and DSCP marking continue to work — shape externally with whatever queueing your platform provides.

## Appliance platforms (MikroTik RouterOS)

When TVPN runs in a container on a router appliance, it usually can’t install queue disciplines itself, because the host kernel doesn’t provide CAKE and can’t be changed. QoS still works — the work just splits across two layers.

**TVPN classifies and marks.** Set `queue-management = "passive"` so it stops attempting to install qdiscs. Classification, link pinning, and DSCP marking on the outer packet all continue to work normally.

**The router shapes.** RouterOS has its own CAKE implementation. Configure a queue with `cake-diffserv=diffserv4` and it acts on the DSCP values TVPN already stamped — no translation rules needed, since the two vocabularies line up directly:

| TVPN class  | DSCP | CAKE tin    |
| ----------- | ---- | ----------- |
| `critical`  | CS6  | Voice       |
| `important` | AF41 | Video       |
| `normal`    | CS0  | Best Effort |
| `bulk`      | CS1  | Bulk        |

High-level steps:

1. Set `queue-management = "passive"` in the `[qos]` section of `traffic-policy.toml` and restart TVPN.
2. Create a CAKE queue type on the router with `cake-diffserv=diffserv4`.
3. Attach it to the interface facing the constrained link, and set the rate limit there rather than in the TVPN policy.
4. Confirm the DSCP marks are arriving before tuning anything else.

Because the router owns the rate limit in this arrangement, `bandwidth-bps` in your TVPN link definitions has no effect.

## Related

- [Configuring Gateway Routes](https://docs.somewear.app/networking/gateway-routes/) — how QoS classification interacts with gateway-served traffic
- [Install TVPN](https://docs.somewear.app/networking/install/linux/) — set up a TVPN endpoint
