Skip to content

Quality of Service (QoS)

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 shown
config-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 = 10
match = { proto = "udp", dst-port = "14550" }
class = "mavlink"
[[rule]]
priority = 20
match = { proto = "udp", dst-port = "5004-5005" }
class = "video"

Configuration options

Global options

These belong in the [qos] section of config.toml:

FieldTypeDefaultNotes
queue-managementstring"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-dscpbooleanfalseWhen 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-egressbooleantrueWhen 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 = false
wash-dscp-egress = true

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

FieldTypeRequiredDefaultNotes
ifacestringyes—OS-level interface name. Linux/macOS: eth0, wlan0, wwan0. Windows: friendly name (e.g. “Ethernet”).
bandwidth-bpsinteger (bits/sec)nounshapedEgress rate cap. Underscores allowed (5_000_000 = 5 Mbit/s). Omit for variable-rate or unknown links.
rtt-msinteger (ms)no100 msAverage 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.

FieldTypeRequiredDefaultNotes
prioritystringyes—One of critical, important, normal, bulk. See the priority values table below.
pin-linkslist of stringsnoempty (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-linkslist of stringsnoemptyIf set, traffic avoids these links, e.g. avoid-links = ["lte"]. Mutually exclusive with pin-links.

Priority values

ValueDescription
criticalSmall, urgent traffic that must get through under congestion.
importantSteady, latency-sensitive flows needing consistent throughput.
normalDefault best-effort traffic with no special urgency.
bulkBackground traffic that yields to everything else.

Rules

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

FieldTypeRequiredDefaultNotes
priorityintegeryes—Evaluation order, 0 to 4_294_967_295. Lower numbers are checked first; the first match wins. Must be unique across all rules.
matchinline tableyes—Match criteria. All specified subfields must match (logical AND). Any subfield you omit is treated as a wildcard. See the match subfields table below.
classstringyes—Name of a defined [class.NAME] to assign matched packets to.

Rule match subfields

FieldTypeRange / ExamplesNotes
protostring (enum)"tcp", "udp", "icmp"Transport protocol. ICMP rules can’t also set src-port or dst-port.
src-ipstring"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-ipstring"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-portstring"14550" or "5004-5005"Single port or inclusive range. Valid ports 1–65535.
dst-portstringsame as src-portSingle port or inclusive range. Valid ports 1–65535.
dscplist 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/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

CapabilityLinuxmacOSFreeBSDWindows
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.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:

tvpn traffic-policy generate

Check what actually loaded:

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:

OSPath
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:

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 classDSCPCAKE tin
criticalCS6Voice
importantAF41Video
normalCS0Best Effort
bulkCS1Bulk

High-level steps:

  1. Set queue-management = "passive" in 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.