Repository navigation
Expand file tree
/
Copy path.golangci.yml
More file actions
363 lines (347 loc) · 16.5 KB
/
Copy path.golangci.yml
File metadata and controls
363 lines (347 loc) · 16.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# .golangci.yml
#
# Tier 1 — Standard lint (~2 minutes). CI gating.
#
# Run:
# lint (inside nix develop)
# nix build .#checks.golangci-lint
#
# Policy: every exclusion below states WHY. When a linter fires, the default
# is to fix the code, not add an exclusion.
version: "2"
run:
timeout: 5m
modules-download-mode: vendor
# Lint the build-tag-gated files too. Without this, golangci-lint only ever
# sees the untagged build, so pkg/xtcp/destinations_{kafka,nats,nsq,valkey,
# s3parquet}.go and pkg/xtcp/enrich_{asn,locality}.go — together a large
# share of the package — are never linted by any tier. Enabling every tag at
# once matches the `all` target in nix/tests/go-test-flavors.nix: the
# registries key on distinct schemes/names, so the union compiles.
build-tags:
- dest_kafka
- dest_nats
- dest_nsq
- dest_valkey
- dest_s3parquet
- enrich_asn
- enrich_locality
formatters:
enable:
- gofmt
- goimports
linters:
default: none
enable:
# Tier 0
- govet
- errcheck
- ineffassign
- unused
- staticcheck
# Tier 1 additions
- gosec
- gocritic
- revive
- noctx
- contextcheck
- durationcheck
# Project-specific: forbid runtime.UnlockOSThread in code paths that
# also do setns/unshare, because the pairing is dangerous (see the
# forbidigo block below for the why).
- forbidigo
# Promoted from Tier 2 on 2026-09-23. US spelling had been swept four
# times (most recently bd3081c) and regressed every time, because
# misspell only ran in the nightly comprehensive tier — nobody saw the
# finding until long after the comment was written. Gating it here
# catches the next `behaviour` on the commit that introduces it.
- misspell
# Promoted from Tier 2 on 2026-10-06, after setRuleAttr's 48 came down to
# 6 and nothing in production code was left over 30. While it ran in Tier 2
# only, `ParseNewRoute` crossed the ceiling TWICE and reached `main` both
# times — not because the tier was unwatched but because its red was
# unreadable: `nix flake check` had other reds, so the exit code was
# already 1 before the regression and 1 after. Gating it here makes a
# function that grows past 30 fail on the commit that grows it, which is
# the only moment the split is still cheap.
- gocyclo
settings:
errcheck:
check-type-assertions: true
check-blank: true
exclude-functions:
- (io.Closer).Close
- (*os.File).Close
# fmt.Fprintf/Fprintln to stdout/stderr writers can't fail in
# practice (writes to *os.File-backed tty fail only on EBADF or
# disk-full for regular files, neither of which we recover from).
- fmt.Fprintf
- fmt.Fprintln
- fmt.Fprint
govet:
enable-all: true
disable:
- fieldalignment
# err shadowing is idiomatic Go and the repo uses it throughout.
# Disable the shadow check rather than rewriting 26 sites just to
# satisfy the linter — the pattern doesn't produce real bugs here.
- shadow
staticcheck:
checks:
- "all"
- "-SA1019"
# US English throughout. Note misspell runs in plain-text mode here
# (`mode` unset), so it reads whole files — comments AND string
# literals — but skips camelCase/PascalCase tokens unconditionally, so
# identifiers and metric label values like `cancelledDuringInit` are
# out of its reach and are not part of this convention.
misspell:
locale: US
gosec:
excludes:
# G103: unsafe pointer use is intentional in pkg/io_uring (giouring wraps
# liburing SQE/CQE structures with unsafe.Pointer).
- G103
# G115: integer overflow conversions appear in netlink parsing (uint16
# length fields) and io_uring batch indices; all are bounds-checked.
- G115
# G204: cmd/ns and cmd/nsTest spawn `ip netns exec ...`; required by design.
- G204
# G304: register_schema reads .proto files whose path comes from CLI flags.
- G304
gocritic:
enabled-checks:
- appendAssign
- argOrder
- badCond
- builtinShadow
- dupCase
- dupSubExpr
- nilValReturn
- rangeValCopy
- sliceClear
disabled-checks:
# hugeParam: triggers on every netlink callback handler that takes a
# NlMsgHdr struct (cheap to copy on hot path; not worth pointer churn).
- hugeParam
revive:
rules:
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: increment-decrement
- name: var-declaration
gocyclo:
# 30, matching .golangci-comprehensive.yml exactly. The number is not
# arbitrary and it is deliberately NOT raised: every finding this linter
# has ever produced in this repo was a switch over kernel attributes that
# had outgrown one function, and in all three cases — ParseNewRoute,
# setLinkAttr and setRuleAttr — the fix was a split that made the code
# better, not a ceiling that made the finding go away. Raising it to
# accommodate the next one would retire the only instrument that has
# noticed.
#
# What the number actually buys, measured on this repo: the next closest
# production function is (RouteView).Text at 26, so there are four points
# of headroom. That is about one `case` plus one `if`. Expect this to
# bite, and treat it as the signal it is.
#
# gocyclo does not count a `default:` arm — measured against
# setLinkDetailAttr, which scores 22 with one present — so a cascade of
# switches linked by their default arms costs nothing per level. That is
# what makes splitting a large attribute switch cheap enough to be the
# standard answer here; see setRuleAttr for the five-level example.
#
# _test.go files are already exempt further down, which is pre-wired for
# exactly this promotion: table-driven tests in this repo are long by
# design and their complexity is in the data, not the control flow.
min-complexity: 30
# Forbidigo guards a specific landmine: `runtime.UnlockOSThread` paired
# with `unix.Setns` / `unix.Unshare`. If the goroutine modifies
# thread-global state (network namespace) and then unconditionally
# unlocks, a *tainted* M can be returned to Go's scheduler — the
# runtime can't safely reuse it, parks it, and spawns a new one.
# Under heavy ns churn this leaks OS threads up to the SetMaxThreads
# cap and crashes the daemon (incident: 12 h s3parquet-long soak hit
# `fatal error: thread exhaustion` at 1 h 45 min).
#
# The safe pattern is: do *not* defer UnlockOSThread; instead, inside
# the deferred restore func, call UnlockOSThread ONLY after Setns
# confirms the original namespace was restored. On restore failure,
# exit the goroutine with the lock still held — the Go runtime then
# terminates the OS thread (documented LockOSThread behaviour),
# which is exactly what we want for a tainted M.
#
# If you have a legitimate non-netns use of UnlockOSThread (e.g.
# io_uring SQ thread pinning), opt in with `//nolint:forbidigo //
# <reason>` at the call site. Anyone touching the netns path will
# then see why the rule exists.
forbidigo:
forbid:
- pattern: '^runtime\.UnlockOSThread$'
msg: |
runtime.UnlockOSThread is unsafe to defer unconditionally in
code that calls unix.Setns / unix.Unshare — the M can be
returned to the scheduler still in a modified namespace,
triggering an unbounded thread leak. Pair it with a
*conditional* unlock inside the restore defer, or
`//nolint:forbidigo // <reason>` if unrelated.
exclusions:
warn-unused: true
paths:
# Skip generated code entirely (don't even type-check it).
- ".*\\.pb\\.go$"
- ".*\\.pb\\.gw\\.go$"
- "^gen/"
rules:
# Test code: relax dup/funlen/gocyclo/errcheck/unused/goconst/noctx.
# Repeated CLI-flag strings in -Args tests + net.Listen/DialTimeout
# without ctx are idiomatic for test setup; lifting them into
# constants or contextual variants adds noise without benefit.
- path: "_test\\.go"
linters:
- dupl
- funlen
- gocyclo
- errcheck
- unused
- goconst
- noctx
# gosec G404 (math/rand): deterministic randomness is the right
# default in tests. G301 (0o755 dir perms): tempdirs created by
# t.TempDir() are already private to the test user; the perms on
# subdirs created inside don't materially affect anything.
#
# G302 (chmod perms) is here for a sharper reason than either: its
# expectation cannot be met by a directory at all. It compares the mode
# against 0600 as a bitmask, so ANY mode carrying the execute bit is a
# finding — and a directory without the execute bit cannot be traversed,
# which makes every traversable directory mode a G302 finding. The tests
# that need it are pkg/listener and pkg/ipasn, where a directory has its
# write bit stripped (0o500) so that os.Remove inside it fails with
# EACCES. That is the only way to reach the unlink errors this phase
# stopped discarding, and 0o500 is strictly more restrictive than the
# 0600 the rule is asking for. The restore to 0o700 afterwards is what
# lets t.TempDir clean up.
#
# Scoped to tests, where it was already scoped. The standalone gosec
# check (nix/checks/go-sec.nix) does not see these at all: gosec defaults
# to -tests=false, so it never reads a _test.go file, which is why no
# equivalent entry belongs in its global -exclude list — putting G302
# there would disable the rule for production code too.
- path: "_test\\.go"
linters:
- gosec
text: "G404|G301|G302"
# gosec G404 (math/rand): the fleet jitter helpers (JitterDuration /
# JitterIntN) intentionally use math/rand/v2 — jitter is a load-spreading
# concern, not a security one, and rand/v2's per-process auto-seeding is
# exactly what the thundering-herd design needs (see
# docs/design-jitter-and-backoff.md). crypto/rand would be wrong here.
- path: "pkg/misc/timing\\.go"
linters:
- gosec
text: "G404"
# Kernel-version-tagged names (TCPInfo6_10_3, TCPInfo5_4_281, …) mirror
# Linux kernel uapi `struct tcp_info` revisions. Renaming breaks the
# explicit per-version mapping that is the point of pkg/xtcpnl.
- path: "pkg/xtcpnl/"
linters:
- staticcheck
text: "ST1003: should not use underscores in Go names; (struct field|const) TCPInfo[0-9_]+(_Size(Cst)?)?"
# ST1003 (naming): protobuf-generated names (PerStream, IO_uring, etc.)
# cannot be changed without breaking wire compatibility.
- linters:
- staticcheck
text: "ST1003:"
# SA9003 (empty branches): allowed for documented "no-op on purpose"
# cases — e.g., netlink message filters that intentionally skip a class.
- linters:
- staticcheck
text: "SA9003:"
# `ifa_prefered` is how the Linux kernel spells it. The comment it appears
# in is a verbatim quotation of `struct ifa_cacheinfo` from
# include/uapi/linux/if_addr.h, and the value of those comments is that
# they diff against the kernel tree — so correcting the spelling would
# make the comment wrong about the thing it documents. Scoped to the one
# word in the one file rather than added to misspell's ignore list, so a
# genuine "prefered" anywhere else still fails.
- path: "pkg/xtcpnl/xtcpnl_ifaddrmsg\\.go"
linters:
- misspell
text: "`prefered` is a misspelling of `preferred`"
# `neighbour` is a command name a user types, not prose. iproute2's object
# table carries both `neighbor` and `neighbour` (ip/ip.c), and `matches()`
# is prefix matching, not fuzzy matching — "neighbour" is not a prefix of
# "neighbor", so dropping the second entry makes `goip neighbour show`
# report an unknown object where `ip` works. internal/goip/dispatch.go
# holds the alias itself, dispatch_test.go names the pair in the comment
# on the row that asserts their order, and obj_neigh_test.go runs the
# alias end to end — that row's argv is the string a user types, so it
# cannot be respelled without testing a command that does not exist.
# Scoped to the one word in those three files, like the `prefered` entry
# above. Prose in those files is still checked, by the audit named in the
# next entry: the exemption there covers only a quoted command token.
- path: "internal/goip/(dispatch(_test)?|obj_neigh_test)\\.go"
linters:
- misspell
text: "`neighbour` is a misspelling of `neighbor`"
# The eleven files below cite kernel paths that contain the word.
# net/core/neighbour.c and include/uapi/linux/neighbour.h are the real
# file names, so "correcting" them produces paths that do not exist and
# breaks the one thing those comments are for, which is diffing against a
# kernel tree. Eighteen occurrences across the eleven; misspell reports
# fifteen, skipping the two inside torvalds/linux URLs, and the last
# is in obj_neigh_test.go, covered by the entry above.
#
# On its own this entry would be exactly the suppression the policy
# forbids. An exclusion can only match misspell's MESSAGE, which is
# identical whether the word sits inside a kernel path or inside British
# prose, so widening to ten whole files does give up real coverage inside
# them. What replaces it is tools/kernel-citation-audit, a check that
# fails unless every occurrence in the tree is directly preceded by
# `net/core/` or `linux/`. That is strictly stronger than what misspell
# was giving: misspell is satisfied by `net/core/neighbor.c` and the audit
# is not, so it also catches a kernel path "fixed" by someone running
# lint-fix.
#
# obj_neigh.go and obj_neigh_test.go are listed in two different entries
# deliberately. The first holds only a citation; the second also holds the
# argv a user types. A collapsed `obj_neigh(_test)?\.go` would match the
# same two files and lose the reason.
- path: "(internal/goip/obj_neigh|internal/goip/render/neigh_test|internal/goip/req/req|internal/goipparity/commands|pkg/nlparity/nlparity_(names|segment)_test|pkg/xtcpnl/xtcpnl_(ndmsg|ndtmsg|rtnetlink_requests)(_test)?)\\.go"
linters:
- misspell
text: "`neighbour` is a misspelling of `neighbor`"
# The audit tool itself, which cannot be written without using the word in
# prose: its documentation, its exemption reasons and its fixtures are all
# about that word. It is also the one directory the audit skips, for the
# same reason, with the argument spelled out at its selfDirCst. The
# alternative was to assemble the constant from fragments so neither tool
# could see it, which hides the word without making anything more correct
# and leaves the file a reader consults for the rule unable to state it.
#
# The text is matched case-insensitively here and nowhere else: the
# fixtures deliberately include `Neighbour` and `NEIGHBOUR`, because the
# audit has to be case-insensitive and a test that only ever passes the
# lowercase form would not show that. The ten citation files above keep
# the exact lowercase text, which is all they contain.
- path: "tools/kernel-citation-audit/"
linters:
- misspell
text: "(?i)`neighbour` is a misspelling of `neighbor`"
issues:
# golangci-lint defaults to max-issues-per-linter: 50 and max-same-issues: 3,
# which silently truncates the report. That is not a cosmetic difference for
# a fix-list: the 2026-09-23 misspell sweep was scoped from a reported "19
# issues" when the true count was 35 — every `behaviour` past the third was
# hidden, in files the capped output never named. A run whose output is the
# work queue has to show all of it.
max-issues-per-linter: 0
max-same-issues: 0
output:
formats:
text:
print-issued-lines: true
print-linter-name: true