Commit 6ee49bebaa
6ee49bebaa449abadc9a5baa0ca0d38d2136acbe
parent: 9c31ca2462
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-28 23:04 UTC
hookd: cap refs in a refused-push row; pack-limit docs name the class cap
Ref #262
Layout: unified · split
.gitbay/wiki/Admin.org
+3 −2
| @@ -215,8 +215,9 @@ push=. |
| 215 | budget: this many at once, this many per account (per client |
215 | budget: this many at once, this many per account (per client |
| 216 | address when anonymous: an IPv4 address, or an IPv6 /64), and this |
216 | address when anonymous: an IPv4 address, or an IPv6 /64), and this |
| 217 | many waiting for at most the wait. Anonymous clients together hold |
217 | many waiting for at most the wait. Anonymous clients together hold |
| 218 | at most =pack_concurrency= − 1 slots when it is above 1, so a signed-in |
218 | at most =pack_concurrency= − 1 slots when it is above 1: an account |
| 219 | client (SSH key, bearer token or web session) can always get the last. |
219 | (SSH key, bearer token or web session) can take the last slot when it |
| |
220 | is free, and anonymous clients cannot hold it. |
| 220 | Past that an SSH client gets "the server is busy…" and exit 1, HTTP |
221 | Past that an SSH client gets "the server is busy…" and exit 1, HTTP |
| 221 | gets 503 with =Retry-After: 30=, git:// an =ERR= line; the daemon |
222 | gets 503 with =Retry-After: 30=, git:// an =ERR= line; the daemon |
| 222 | logs a =pack limit= warning naming the transport and whether the |
223 | logs a =pack limit= warning naming the transport and whether the |
.gitbay/wiki/Performance.org
+7 −4
| @@ -55,7 +55,10 @@ The defaults (=pack_concurrency= 3, =pack_per_principal= 2, |
| 55 | =pack_queue= 32, =pack_queue_wait= 60s) are set for a four-core host |
55 | =pack_queue= 32, =pack_queue_wait= 60s) are set for a four-core host |
| 56 | from the single-clone figure above; no concurrent-clone measurement |
56 | from the single-clone figure above; no concurrent-clone measurement |
| 57 | backs them yet. =deploy/clonebench.sh <clone-url> <n>= starts n full |
57 | backs them yet. =deploy/clonebench.sh <clone-url> <n>= starts n full |
| 58 | clones at once from a machine other than the server. Clones from one |
58 | clones at once from a machine other than the server. Anonymous clones |
| 59 | address count against one principal, so run it from two addresses, or |
59 | together hold at most =pack_concurrency= − 1 slots, and clones from one |
| 60 | against an instance with =pack_per_principal = -1=, or it measures the |
60 | address or account count against one principal, so an anonymous run |
| 61 | per-principal cap instead of the global one. |
61 | measures those caps rather than the global one. Run it as signed-in |
| |
62 | clients (SSH keys, or HTTP with bearer tokens) from more than one |
| |
63 | account, or against an instance with =pack_per_principal = -1= and |
| |
64 | read the result as the anonymous class cap. |
CHANGELOG.org
+3 −2
| @@ -238,8 +238,9 @@ missing, =gitbayd admin backup --verify <archive>= names it, and |
| 238 | download= are unaffected. Under =ssh.mode = "system"= SSH clones are not counted, |
238 | download= are unaffected. Under =ssh.mode = "system"= SSH clones are not counted, |
| 239 | since each session is its own process (#262). |
239 | since each session is its own process (#262). |
| 240 | - Anonymous clones are counted per IPv4 address or IPv6 /64, and |
240 | - Anonymous clones are counted per IPv4 address or IPv6 /64, and |
| 241 | together hold at most =pack_concurrency= − 1 slots, so a signed-in |
241 | together hold at most =pack_concurrency= − 1 slots: an account can |
| 242 | client can always get the last one (#262). |
242 | take the last slot when it is free, and anonymous clients cannot hold |
| |
243 | it (#262). |
| 243 | - A request turned away by the pack limit logs a warning naming the |
244 | - A request turned away by the pack limit logs a warning naming the |
| 244 | transport and whether the client was signed in, never its address, |
245 | transport and whether the client was signed in, never its address, |
| 245 | at most once a minute per transport (#262). |
246 | at most once a minute per transport (#262). |
internal/hookd/hookd.go
+12 −4
| @@ -172,14 +172,22 @@ func (s *Server) authorize(req Request) (actor int64, msg string) { |
| 172 | // peerCheck is checkPeer; tests replace it. |
172 | // peerCheck is checkPeer; tests replace it. |
| 173 | var peerCheck = checkPeer |
173 | var peerCheck = checkPeer |
| 174 | |
174 | |
| |
175 | // auditedRefs is how many ref names a refused-push row keeps; the rest |
| |
176 | // are counted, so one push of many refs cannot write an unbounded row. |
| |
177 | const auditedRefs = 20 |
| |
178 | |
| 175 | // refusePush answers a pre-receive refusal and audits it. |
179 | // refusePush answers a pre-receive refusal and audits it. |
| 176 | func (s *Server) refusePush(enc *json.Encoder, req Request, repo store.Repo, msg string) { |
180 | func (s *Server) refusePush(enc *json.Encoder, req Request, repo store.Repo, msg string) { |
| 177 | refs := make([]string, len(req.Updates)) |
181 | n := min(len(req.Updates), auditedRefs) |
| 178 | for i, u := range req.Updates { |
182 | refs := make([]string, n) |
| |
183 | for i, u := range req.Updates[:n] { |
| 179 | refs[i] = u.Ref |
184 | refs[i] = u.Ref |
| 180 | } |
185 | } |
| 181 | control.AuditRefused(s.st, req.UserID, "refused push", |
186 | data := map[string]any{"repo": repo.Path(), "refs": refs, "reason": msg} |
| 182 | map[string]any{"repo": repo.Path(), "refs": refs, "reason": msg}) |
187 | if more := len(req.Updates) - n; more > 0 { |
| |
188 | data["more_refs"] = more |
| |
189 | } |
| |
190 | control.AuditRefused(s.st, req.UserID, "refused push", data) |
| 183 | enc.Encode(Response{Allow: false, Message: msg}) |
191 | enc.Encode(Response{Allow: false, Message: msg}) |
| 184 | } |
192 | } |
| 185 | |
193 | |
internal/hookd/socket_test.go
+34
| @@ -1,7 +1,9 @@ |
| 1 | package hookd |
1 | package hookd |
| 2 | |
2 | |
| 3 | import ( |
3 | import ( |
| |
4 | "encoding/json" |
| 4 | "errors" |
5 | "errors" |
| |
6 | "fmt" |
| 5 | "net" |
7 | "net" |
| 6 | "os" |
8 | "os" |
| 7 | "path/filepath" |
9 | "path/filepath" |
| @@ -161,3 +163,35 @@ func TestPeerRefusalIsAudited(t *testing.T) { |
| 161 | t.Fatalf("refused hook rows: %+v", rows) |
163 | t.Fatalf("refused hook rows: %+v", rows) |
| 162 | } |
164 | } |
| 163 | } |
165 | } |
| |
166 | |
| |
167 | // A refused push of many refs records the first auditedRefs names and |
| |
168 | // a count of the rest. |
| |
169 | func TestRefusedPushCapsRefs(t *testing.T) { |
| |
170 | sock, st, repoID, uid := serveSocket(t) |
| |
171 | token, err := st.CreatePushToken(repoID, uid, "full") |
| |
172 | if err != nil { |
| |
173 | t.Fatal(err) |
| |
174 | } |
| |
175 | req := Request{Hook: "pre-receive", RepoID: repoID, UserID: uid, Scope: "full", Token: token} |
| |
176 | for i := range 500 { |
| |
177 | req.Updates = append(req.Updates, policy.RefUpdate{Ref: fmt.Sprintf("refs/merge-requests/%d/head", i), |
| |
178 | Old: zeroSHA40, New: strings.Repeat("a", 40)}) |
| |
179 | } |
| |
180 | if resp, err := Ask(sock, req, nil); err != nil || resp.Allow { |
| |
181 | t.Fatalf("push: %+v, %v", resp, err) |
| |
182 | } |
| |
183 | rows := refusedRows(t, st, "refused push") |
| |
184 | if len(rows) != 1 { |
| |
185 | t.Fatalf("rows: %+v", rows) |
| |
186 | } |
| |
187 | var data struct { |
| |
188 | Refs []string `json:"refs"` |
| |
189 | MoreRefs int `json:"more_refs"` |
| |
190 | } |
| |
191 | if err := json.Unmarshal([]byte(rows[0].Data), &data); err != nil { |
| |
192 | t.Fatal(err) |
| |
193 | } |
| |
194 | if len(data.Refs) != auditedRefs || data.MoreRefs != 500-auditedRefs || data.Refs[0] != "refs/merge-requests/0/head" { |
| |
195 | t.Fatalf("refs %d, more %d", len(data.Refs), data.MoreRefs) |
| |
196 | } |
| |
197 | } |