go-tc puts Linux traffic control inside your Go process
traffic control in pure go - it allows to read and alter queues, filters and classes
At a glance
- What is it?
- go-tc is a Go library that talks to the rtnetlink traffic control system directly, giving Go programs a typed way to read and alter qdiscs, filters and classes without a C binding. The README itself calls it a work in progress version of tc, and the tagged releases trail the last commit.
- Who is it for?
- go-tc fits teams that already run Go services and need to inspect or rewrite qdiscs, filters and classes in process, and it fits almost nobody else, since there is no command line entry point and the README calls the project a work in progress.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A netlink socket instead of a tc subprocess
The README describes go-tc as a work in progress version of tc that provides a C binding free API to the netlink based traffic control system of rtnetlink. That single sentence sets the whole scope. There is no binary to install and no command line surface; you import the package, open a socket, and issue typed calls from inside your own program.
The audience is engineers who already run Go on Linux hosts and need traffic control as part of an application, a controller or a test. A monitoring agent that wants to report which qdisc sits on each interface, or a test that needs to install a queue discipline and then remove it, is the kind of job this library is shaped for. Anything that is a one-off command typed by a human on a server is a poor fit, because nothing here produces a command line interface.
The visible surface is broad but specific. The repository root carries one file per kernel attribute family, including attributeTcMsg.go, chain.go, class.go, actions.go, ematch.go and a set of f_ prefixed filter files such as f_basic.go, f_bpf.go, f_cgroup.go, f_flow.go and f_flower.go. Queues, filters, classes, chains and actions are all addressed, and the project topics list bpf, qdisc, qos, rtnetlink and traffic control.
CAP_NET_ADMIN is part of the API contract
Because the package processes information directly from the kernel, the README states that it requires special privileges and points at adjusting the CAP_NET_ADMIN capabilities. The example it gives attaches the capability to a built executable rather than running the program as root:
setcap 'cap_net_admin=+ep' /your/executableReplace the placeholder path with the binary you built, and remember that the attribute lives on the file, so rebuilding or moving the binary changes what it carries. For a container that is a one line Dockerfile change; for a systemd unit it is a step in the unit definition. The point worth flagging is that this is a deliberate narrowing: the program keeps its own user identity and gains exactly one capability.
The library also exposes a kernel option for better diagnostics. The README notes that NETLINK_EXT_ACK is supported since the 4.12 kernel and that unix.ENOPROTOOPT is returned when it is not, and the example enables it with a single call:
err = rtnl.SetOption(netlink.ExtendedAcknowledge, true)On an old kernel that call is a cheap probe. If it fails with ENOPROTOOPT, the failure is the kernel refusing the option rather than a bug in your traffic control code.
The Open, Get and InterfaceByIndex sequence behind the first example
The example in the README is short and does one thing: it lists the qdisc on every interface. The first step opens the rtnetlink socket and returns a handle that owns it.
rtnl, err := tc.Open(&tc.Config{})
if err != nil {
fmt.Fprintf(os.Stderr, "could not open rtnetlink socket: %v\n", err)
return
}The second step asks that handle for qdiscs across all interfaces, and the example checks the error before touching the result.
qdiscs, err := rtnl.Qdisc().Get()
if err != nil {
fmt.Fprintf(os.Stderr, "could not get qdiscs: %v\n", err)
return
}Each returned qdisc carries an interface index and a kind, and the example resolves the index to a name before printing:
for _, qdisc := range qdiscs {
iface, err := net.InterfaceByIndex(int(qdisc.Ifindex))
if err != nil {
fmt.Fprintf(os.Stderr, "could not get interface from id %d: %v", qdisc.Ifindex, err)
return
}
fmt.Printf("%20s\t%s\n", iface.Name, qdisc.Kind)
}That loop is the whole read path: a query object built from the handle, a Get call that returns a slice, and a second lookup through the standard library to turn a numeric index into a device name. The same shape applies to the other object types, with Qdisc() giving way to class, filter, chain and action accessors. Note that the README points to the package documentation for more and advanced examples, so the file you are reading is the floor, not the ceiling.
The dependency list shows where the feature line sits
The go.mod file is short and its header tells you the module path and the language floor.
module github.com/florianl/go-tc
go 1.18A go directive of 1.18 means the library does not force a bleeding edge toolchain on you, and the README only asks for a Go version that upstream still supports. The direct requirements are the interesting part:
require (
github.com/cilium/ebpf v0.8.1
github.com/google/go-cmp v0.6.0
github.com/josharian/native v1.2.0
github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786
github.com/mdlayher/netlink v1.7.1
golang.org/x/sys v0.7.0
)The cilium/ebpf dependency lines up with the f_bpf.go file and the bpf and ebpf topics on the repository, which is how a filter that carries a BPF program gets encoded. josharian/native handles the byte order of netlink attributes, and mdlayher/netlink provides the socket plumbing and the ExtendedAcknowledge option shown earlier. jsimonetti/rtnetlink is pinned to a pseudo version from 2021, and mdlayher/netlink sits at v1.7.1, so the pinned graph is not bleeding edge. Three indirect requirements, mdlayher/socket, golang.org/x/net and golang.org/x/sync, are pulled in by the two netlink packages. Because Go resolves the shared graph across your whole build, a service that already requires a newer mdlayher/netlink will lift the version here too, and that is the first place to look when a build behaves differently than it did in a standalone example.
The file tree is a map of the kernel's tc vocabulary
Reading the repository layout is more informative here than reading the README, because the README documents only the opening example. Each root file maps to a concept the kernel exposes. attributeTcMsg.go is the request and response structure that every tc netlink message shares. class.go and chain.go cover the hierarchy of classes and filter chains. actions.go handles the action lists that a filter can trigger.
The ematch family is the classifier layer, and it is split by matching type rather than by one file: ematch_cmp.go, ematch_container.go, ematch_ipset.go, ematch_ipt.go, ematch_meta.go, ematch_nbyte.go and ematch_u32.go. ematch.go holds the shared container. A filter can be attached without a match expression at all, and the tree shows the machinery for each flavor of matching the kernel offers.
Filter types have their own files, f_basic.go, f_bpf.go, f_cgroup.go, f_flow.go and f_flower.go, and a core/ directory sits at the root. The example test files are the best coverage guide in the repository: example_netem_test.go, example_htb_test.go, example_hfsc_test.go, example_tbf_test.go, example_fqCodel_test.go, example_matchall_test.go, example_flower_test.go, example_ebpf_test.go, example_ematch_ipset_test.go, example_monitor_test.go, example_qdisc_replace_test.go and example_actions_test.go. They name the queue disciplines and filters the author expected people to reach for. Treat that list as the practical boundary of the library rather than as a complete matrix, since the tree is a snapshot of one point in time.
The README's own wording, and what the tags say about it
The project states its own status plainly in the opening line: this is a work in progress version of tc. That sentence deserves more weight than a badge would, because it tells you the feature set is expected to change and that some corners may be unfinished.
The repository is not archived, and the last push was on 2026-10-17, so work is landing. The tags tell a slower story: v0.4.5 was released on 2025-03-15, v0.4.4 on 2024-07-24 and v0.4.3 on 2024-01-06. The gap between the newest tag and the newest commit is the practical upgrade question. If you depend on a released version, your dependency is v0.4.5 and any change after 2025-03-15 is not in it. If you track the main branch you get everything the author has pushed, including whatever is still in progress, with no tag to fall back on. Pinning to v0.4.5 is the safer default for a service that must not change behaviour underneath itself.
Two other files matter. LICENSE is MIT, so there is no copyleft obligation attached to code you link into a proprietary service, though the licence text and its conditions are the ones to read rather than a summary of them. SECURITY.md sits at the top level, which tells you the project has a place to report problems, though the file contents are not reproduced here and you would need to read it before filing anything.
Where a Go library stops being the right tool
The clearest limitation is coverage and shape. The README documents one operation end to end, the listing of qdiscs, and defers everything else to the package documentation on pkg.go.dev. The kernel's traffic control surface keeps growing, and a library that describes itself as a work in progress will trail the reference implementation in some corner, whether that is a newly added qdisc option or a filter attribute the author has not wired up yet. There is no command line fallback and no binary to diff against, so a feature you cannot reach in the library is a feature you must reach some other way.
The second limitation is operational. Everything happens in process, which is the point, but it also means the caller must be prepared to hold CAP_NET_ADMIN and to interpret netlink error values. A one-off inspection on a host where nobody wants to install a Go toolchain is better served by the tc system that the README itself links to through the tc.8 manual page, and that reference implementation remains the yardstick for what the full feature set looks like. go-tc is the in-process equivalent, not a superset.
A third case: if you need to reconcile a large fleet, the library gives you the verbs but no reporting, no retry policy and no state store. That work lands in your code, and every engineer on the team inherits it.
Editorial conclusion
go-tc fits teams that already run Go services and need to inspect or rewrite qdiscs, filters and classes in process, and it fits almost nobody else, since there is no command line entry point and the README calls the project a work in progress. Before you adopt it, confirm that the specific qdisc or filter kinds you depend on are covered by the example tests in the repository, and pin to a tag rather than the branch, because the newest release is v0.4.5 from 2025-03-15 while the last push was on 2026-10-17.
Frequently asked questions
How do I install go-tc and what Go version does it need?
It is an ordinary Go module with the path github.com/florianl/go-tc, and its go.mod declares go 1.18, so any toolchain Go still supports will build it. The README states the requirement simply as a version of Go that is supported by upstream.
Does go-tc need root privileges to read qdiscs?
Yes. The README says the package processes information directly from the kernel and therefore requires special privileges, which can be provided by adjusting the CAP_NET_ADMIN capabilities, for example with setcap 'cap_net_admin=+ep' on your executable.
How do I get detailed error messages from the kernel in go-tc?
Enable the NETLINK_EXT_ACK option with rtnl.SetOption(netlink.ExtendedAcknowledge, true). The README notes that this is supported since the 4.12 kernel and that unix.ENOPROTOOPT is returned when the kernel does not support it.
Is go-tc still being worked on, and which version should I pin?
The repository is not archived and the last push was on 2026-10-17, but the most recent release is v0.4.5 from 2025-03-15, so recent work is not in any tag. The README calls the project a work in progress version of tc, so pinning a tag is the cautious choice.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/florianl-go-tc)Was this useful?