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:
[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.
[[rule]]priority = 10match = { proto = "udp", dst-port = "14550" }class = "mavlink"
[[rule]]priority = 20match = { 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.
[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.
[link.lte]iface = "eth0"bandwidth-bps = 5_000_000 # ~5 Mbit/s upstreamrtt-ms = 75
[link.starlink]iface = "eth1"bandwidth-bps = 15_000_000 # ~15 Mbit/s upstreamrtt-ms = 40Configuration 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. |
[qos]queue-management = "active" # "active" | "passive" (default "active")passthrough-inner-dscp = falsewash-dscp-egress = trueLinks
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. |
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
# Classify important TCP traffic[class.tak]priority = "important"
[[rule]]priority = 30# Without proto = "tcp", the rule would also match UDP/8087match = { proto = "tcp", dst-port = "8087" }class = "tak"
# Deprioritize certain traffic[class.uploads]priority = "bulk"
[[rule]]priority = 40match = { 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 = 50match = { 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:
tvpn traffic-policy validate /var/lib/somewear/tvpn/traffic-policy.tomlThis 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:
tvpn traffic-policy generateCheck what actually loaded:
tvpn traffic-policy showTVPN 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-linksandavoid-linksset proto = "icmp"combined withsrc-portordst-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:
modinfo sch_cakeIf 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:
- Set
queue-management = "passive"in the[qos]section oftraffic-policy.tomland restart TVPN. - Create a CAKE queue type on the router with
cake-diffserv=diffserv4. - Attach it to the interface facing the constrained link, and set the rate limit there rather than in the TVPN policy.
- 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 — how QoS classification interacts with gateway-served traffic
- Install TVPN — set up a TVPN endpoint