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; default shownconfig-path = "/var/lib/somewear/tvpn/traffic-policy.toml"traffic-policy.toml:
[link.lte]iface = "eth0"bandwidth-bps = 5_000_000 # ~5 Mbit/s upstream (LTE)rtt-ms = 75
[link.starlink]iface = "eth1"bandwidth-bps = 15_000_000 # ~15 Mbit/s upstream (Starlink)rtt-ms = 40
[class.mavlink]priority = "critical"
[class.video]priority = "important"pin-links = ["starlink"]
[class.default]priority = "normal"
[[rule]]priority = 10match = { proto = "udp", dst-port = "14550" }class = "mavlink"
[[rule]]priority = 20match = { proto = "udp", dst-port = "5004-5005" }class = "video"Configuration options
Global options
These belong in the [qos] section of config.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. 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"intraffic-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 — QoS requires a Linux-based TVPN endpoint