2026-10-10 09:57:43 +02:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
2026-10-10 10:19:39 +02:00
|
|
|
"context"
|
2026-10-10 09:57:43 +02:00
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
2026-10-10 10:19:39 +02:00
|
|
|
"fmt"
|
2026-10-10 09:57:43 +02:00
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
2026-10-10 10:18:40 +02:00
|
|
|
"sync"
|
|
|
|
|
"time"
|
2026-10-10 09:57:43 +02:00
|
|
|
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
"github.com/git-pkgs/proxy/internal/database"
|
2026-10-10 09:57:43 +02:00
|
|
|
"github.com/git-pkgs/proxy/internal/metrics"
|
|
|
|
|
"golang.org/x/mod/module"
|
|
|
|
|
"golang.org/x/mod/semver"
|
|
|
|
|
"golang.org/x/mod/sumdb/tlog"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// defaultGoSumDBName is the checksum database the go command uses when
|
|
|
|
|
// GOSUMDB is not set.
|
|
|
|
|
defaultGoSumDBName = "sum.golang.org"
|
|
|
|
|
defaultGoSumDBURL = "https://sum.golang.org"
|
|
|
|
|
goEcosystem = "golang"
|
2026-10-10 10:18:40 +02:00
|
|
|
|
|
|
|
|
// goSumDBProbeBackoff is how long supported keeps answering 404 after the
|
|
|
|
|
// proxy failed to reach a checksum database, before it tries again.
|
|
|
|
|
goSumDBProbeBackoff = time.Minute
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
// goSumDBProbeTimeout is how long supported waits for the database. The
|
|
|
|
|
// go command's own request has no timeout, so a database that drops
|
|
|
|
|
// packets must not hold every build for the full upstream timeout.
|
|
|
|
|
goSumDBProbeTimeout = 5 * time.Second
|
2026-10-10 09:57:43 +02:00
|
|
|
)
|
|
|
|
|
|
2026-10-10 10:18:40 +02:00
|
|
|
// goSumDBProbes remembers when the proxy last failed to reach each checksum
|
|
|
|
|
// database, so a database that is unreachable does not make every go command
|
|
|
|
|
// wait for an upstream timeout before it falls back.
|
|
|
|
|
type goSumDBProbes struct {
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
// timeout overrides goSumDBProbeTimeout when set (tests).
|
|
|
|
|
timeout time.Duration
|
|
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
failed map[string]time.Time
|
|
|
|
|
succeeded map[string]time.Time
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *goSumDBProbes) probeTimeout() time.Duration {
|
|
|
|
|
if p.timeout > 0 {
|
|
|
|
|
return p.timeout
|
|
|
|
|
}
|
|
|
|
|
return goSumDBProbeTimeout
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *goSumDBProbes) recentlyFailed(name string) bool {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
failedAt, ok := p.failed[name]
|
|
|
|
|
return ok && time.Since(failedAt) < goSumDBProbeBackoff
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (p *goSumDBProbes) record(name string, reachable bool) {
|
|
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
if reachable {
|
|
|
|
|
delete(p.failed, name)
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
if p.succeeded == nil {
|
|
|
|
|
p.succeeded = make(map[string]time.Time)
|
|
|
|
|
}
|
|
|
|
|
p.succeeded[name] = time.Now()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if p.failed == nil {
|
|
|
|
|
p.failed = make(map[string]time.Time)
|
|
|
|
|
}
|
|
|
|
|
p.failed[name] = time.Now()
|
|
|
|
|
}
|
|
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// recordFailure records that the database did not answer a request started
|
|
|
|
|
// at start, unless a probe has reached it since: a probe that timed out, or
|
|
|
|
|
// a lookup that fell back to the cache, must not overwrite a newer success.
|
|
|
|
|
func (p *goSumDBProbes) recordFailure(name string, start time.Time) {
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
p.mu.Lock()
|
|
|
|
|
defer p.mu.Unlock()
|
|
|
|
|
if !p.succeeded[name].Before(start) {
|
2026-10-10 10:18:40 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if p.failed == nil {
|
|
|
|
|
p.failed = make(map[string]time.Time)
|
|
|
|
|
}
|
|
|
|
|
p.failed[name] = time.Now()
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 09:57:43 +02:00
|
|
|
// SetSumDBs sets the checksum databases served at /sumdb/{name}/, mapping
|
|
|
|
|
// each database name to its base URL. An empty map keeps the default,
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// sum.golang.org. offline makes a cached tree of any age enough to keep
|
|
|
|
|
// answering for a database the proxy cannot reach, for networks where
|
|
|
|
|
// clients have no route to the database of their own.
|
|
|
|
|
func (h *GoHandler) SetSumDBs(sumDBs map[string]string, offline bool) {
|
|
|
|
|
h.sumDBOffline = offline
|
2026-10-10 09:57:43 +02:00
|
|
|
if len(sumDBs) == 0 {
|
|
|
|
|
h.sumDBs = map[string]string{defaultGoSumDBName: defaultGoSumDBURL}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
h.sumDBs = make(map[string]string, len(sumDBs))
|
|
|
|
|
for name, sumDBURL := range sumDBs {
|
|
|
|
|
h.sumDBs[name] = strings.TrimSuffix(sumDBURL, "/")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// handleSumDB serves checksum database requests that the go command sends
|
|
|
|
|
// through GOPROXY as /sumdb/{name}/{path}. The module proxy upstream does not
|
|
|
|
|
// serve them, so they are fetched from the configured database itself.
|
|
|
|
|
// Unknown names get 404, which makes the go command contact the database
|
|
|
|
|
// directly.
|
|
|
|
|
func (h *GoHandler) handleSumDB(w http.ResponseWriter, r *http.Request, rest string) {
|
|
|
|
|
name, path, _ := strings.Cut(rest, "/")
|
|
|
|
|
sumDBURL, ok := h.sumDBs[name]
|
|
|
|
|
if !ok {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if path == "supported" {
|
2026-10-10 10:18:40 +02:00
|
|
|
if h.sumDBReachable(r, name, sumDBURL) {
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
} else {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
immutable, valid := parseGoSumDBPath(path)
|
|
|
|
|
if !valid {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-10-13 16:23:12 +02:00
|
|
|
// Only mirrors and auditors fetch data tiles, never the go command, and
|
|
|
|
|
// together they hold the whole log, so they are not stored.
|
|
|
|
|
if tile, err := tlog.ParseTilePath(path); err == nil && tile.L < 0 {
|
|
|
|
|
h.proxy.proxyMetadataStream(w, r, sumDBURL+"/"+path, "identity", "*/*")
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
|
|
|
|
|
cacheKey := goSumDBCacheKey(name, sumDBURL, path)
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
check := goSumDBResponseCheck(path)
|
2026-10-10 10:19:39 +02:00
|
|
|
served, damaged := false, false
|
|
|
|
|
if immutable {
|
|
|
|
|
served, damaged = h.serveCachedSumDBTile(w, r, cacheKey, check)
|
|
|
|
|
}
|
|
|
|
|
if served {
|
2026-10-10 09:57:43 +02:00
|
|
|
return
|
|
|
|
|
}
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.proxySumDB(w, r, name, sumDBURL+"/"+path, cacheKey, check, damaged)
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-10 10:18:40 +02:00
|
|
|
// sumDBReachable reports whether the proxy can serve a checksum database: it
|
|
|
|
|
// can fetch the latest signed tree, or has it cached. Once supported answers
|
|
|
|
|
// 200 the go command uses only the proxy for that database and never falls
|
|
|
|
|
// back to contacting it directly, so a database the proxy cannot reach gets
|
|
|
|
|
// 404 instead. The probe goes through the same cache entry as a latest
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
// request: with cache_metadata it costs at most one upstream request per
|
|
|
|
|
// metadata TTL, without it each probe fetches latest.
|
2026-10-10 10:18:40 +02:00
|
|
|
func (h *GoHandler) sumDBReachable(r *http.Request, name, sumDBURL string) bool {
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
cacheKey := goSumDBCacheKey(name, sumDBURL, "latest")
|
|
|
|
|
treeCheck := goSumDBResponseCheck("latest")
|
2026-10-10 10:18:40 +02:00
|
|
|
if h.sumDBProbes.recentlyFailed(name) {
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
return h.cachedTreeServes(r.Context(), cacheKey, treeCheck)
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
// The fetch is shared with concurrent callers and runs detached from this
|
|
|
|
|
// request, so it is bounded by the upstream timeout, not by ours. Wait
|
|
|
|
|
// for it only up to the probe timeout; it finishes in the background,
|
|
|
|
|
// records its own outcome and fills the cache for the next probe.
|
|
|
|
|
start := time.Now()
|
|
|
|
|
done := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
done <- h.probeSumDB(context.WithoutCancel(r.Context()), name, cacheKey, sumDBURL, treeCheck)
|
|
|
|
|
}()
|
|
|
|
|
var err error
|
|
|
|
|
select {
|
|
|
|
|
case err = <-done:
|
|
|
|
|
case <-time.After(h.sumDBProbes.probeTimeout()):
|
|
|
|
|
err = fmt.Errorf("no answer within %s", h.sumDBProbes.probeTimeout())
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
h.sumDBProbes.recordFailure(name, start)
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
case <-r.Context().Done():
|
2026-10-10 10:18:40 +02:00
|
|
|
// The client went away; that says nothing about the database.
|
|
|
|
|
return false
|
|
|
|
|
}
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
if err == nil {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
// The stale-cache fallback inside the fetch only runs once upstream has
|
|
|
|
|
// failed, which a silent database delays past the probe timeout. A tree
|
|
|
|
|
// cached earlier may still serve lookups, so look for it here.
|
|
|
|
|
if h.cachedTreeServes(r.Context(), cacheKey, treeCheck) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
h.proxy.Logger.Info("checksum database unreachable, sending go clients to it directly",
|
|
|
|
|
"name", name, "url", sumDBURL, "error", err)
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// probeSumDB fetches the database's latest tree and records whether the
|
2026-10-12 22:35:23 +02:00
|
|
|
// database answered. A tree still fresh in the cache is enough for supported
|
|
|
|
|
// but records nothing, since the database was not asked: counting it as an
|
|
|
|
|
// answer would keep lookups that fail during an outage from being
|
|
|
|
|
// remembered. A tree served because upstream failed is not an answer.
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
func (h *GoHandler) probeSumDB(ctx context.Context, name, cacheKey, sumDBURL string, check func([]byte) error) error {
|
2026-10-12 22:35:23 +02:00
|
|
|
if entry, _, _, ok := h.readCachedSumDB(ctx, cacheKey, check); ok && h.treeFresh(entry) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
res := h.proxy.lookupMetadata(ctx, goEcosystem, cacheKey, sumDBURL+"/latest", "*/*", "identity", check)
|
|
|
|
|
err := res.err
|
|
|
|
|
if err == nil && res.stale {
|
|
|
|
|
err = errors.New("checksum database did not answer, tree served from cache")
|
|
|
|
|
}
|
2026-10-10 10:18:40 +02:00
|
|
|
h.sumDBProbes.record(name, err == nil)
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cachedTreeServes reports whether a cached tree lets the proxy answer for
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// the database although the database did not: always with go_sumdbs_offline,
|
|
|
|
|
// otherwise only while the tree is fresh.
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
func (h *GoHandler) cachedTreeServes(ctx context.Context, cacheKey string, check func([]byte) error) bool {
|
|
|
|
|
entry, _, _, ok := h.readCachedSumDB(ctx, cacheKey, check)
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
return ok && (h.sumDBOffline || h.treeFresh(entry))
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// treeFresh reports whether a cached tree is within the metadata TTL.
|
|
|
|
|
func (h *GoHandler) treeFresh(entry *database.MetadataCacheEntry) bool {
|
|
|
|
|
ttl := h.proxy.MetadataTTL
|
|
|
|
|
return ttl > 0 && entry.FetchedAt.Valid && time.Since(entry.FetchedAt.Time) < ttl
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// readCachedSumDB reads a checksum database response from the metadata
|
|
|
|
|
// cache regardless of its age. It reports false when nothing usable is
|
|
|
|
|
// cached.
|
|
|
|
|
func (h *GoHandler) readCachedSumDB(ctx context.Context, cacheKey string, check func([]byte) error) (entry *database.MetadataCacheEntry, body []byte, contentType string, ok bool) {
|
|
|
|
|
p := h.proxy
|
|
|
|
|
if !p.CacheMetadata || p.DB == nil {
|
|
|
|
|
return nil, nil, "", false
|
|
|
|
|
}
|
|
|
|
|
entry, err := p.DB.GetMetadataCache(goEcosystem, cacheKey)
|
|
|
|
|
if err != nil || entry == nil {
|
|
|
|
|
return nil, nil, "", false
|
|
|
|
|
}
|
|
|
|
|
body, contentType, err = p.readCachedMetadata(ctx, entry, check)
|
2026-10-10 10:18:40 +02:00
|
|
|
if err != nil {
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
return nil, nil, "", false
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
return entry, body, contentType, true
|
2026-10-10 10:18:40 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-10 09:57:43 +02:00
|
|
|
// parseGoSumDBPath reports whether path is a request the checksum database
|
|
|
|
|
// protocol defines and whether its response can never change. Only complete
|
|
|
|
|
// tiles are immutable; partial tiles, latest and lookups follow the metadata
|
|
|
|
|
// TTL.
|
|
|
|
|
func parseGoSumDBPath(path string) (immutable, valid bool) {
|
|
|
|
|
if path == "latest" {
|
|
|
|
|
return false, true
|
|
|
|
|
}
|
|
|
|
|
if mod, ok := strings.CutPrefix(path, "lookup/"); ok {
|
|
|
|
|
return false, validGoSumDBLookup(mod)
|
|
|
|
|
}
|
|
|
|
|
tile, err := tlog.ParseTilePath(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, false
|
|
|
|
|
}
|
|
|
|
|
return tile.W == 1<<tile.H, true
|
|
|
|
|
}
|
|
|
|
|
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
// goSumDBTreePrefix starts every signed tree a checksum database serves, as
|
|
|
|
|
// latest and after each lookup record (tlog.FormatTree).
|
|
|
|
|
const goSumDBTreePrefix = "go.sum database tree\n"
|
|
|
|
|
|
|
|
|
|
// goSumDBResponseCheck returns a check for a response to path, or nil when
|
|
|
|
|
// path is not one the protocol defines. A 200 that is not what the protocol
|
|
|
|
|
// defines (a block page from an egress proxy, a cut-off response) is refused
|
|
|
|
|
// rather than cached. latest must look like a signed tree, since supported
|
|
|
|
|
// counts it as proof the database is reachable. A lookup must be a record
|
|
|
|
|
// followed by a signed tree, which is how the go command reads it; a block
|
|
|
|
|
// page cached in its place would also replace the copy kept for an outage.
|
|
|
|
|
// A hash tile holds exactly W hashes, and complete tiles are never
|
2026-10-13 16:23:12 +02:00
|
|
|
// revalidated, so a bad one would otherwise stay for good. Data tiles are
|
|
|
|
|
// never cached and not checked.
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
func goSumDBResponseCheck(path string) func([]byte) error {
|
|
|
|
|
if path == "latest" {
|
|
|
|
|
return func(body []byte) error {
|
|
|
|
|
if !strings.HasPrefix(string(body), goSumDBTreePrefix) {
|
|
|
|
|
return errors.New("checksum database latest is not a signed tree")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(path, "lookup/") {
|
|
|
|
|
return func(body []byte) error {
|
|
|
|
|
_, _, tree, err := tlog.ParseRecord(body)
|
|
|
|
|
if err != nil || !strings.HasPrefix(string(tree), goSumDBTreePrefix) {
|
|
|
|
|
return fmt.Errorf("checksum database lookup %s is not a record with a signed tree", path)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-10 10:19:39 +02:00
|
|
|
tile, err := tlog.ParseTilePath(path)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return func(body []byte) error {
|
2026-10-13 16:23:12 +02:00
|
|
|
if len(body) != tile.W*tlog.HashSize {
|
2026-10-10 10:19:39 +02:00
|
|
|
return fmt.Errorf("checksum database tile %s has %d bytes, want %d", path, len(body), tile.W*tlog.HashSize)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 09:57:43 +02:00
|
|
|
// validGoSumDBLookup checks an escaped module@version the way the checksum
|
|
|
|
|
// database does. Requiring a semantic version also keeps characters such as
|
|
|
|
|
// '#' and '%' out of the upstream URL.
|
|
|
|
|
func validGoSumDBLookup(mod string) bool {
|
|
|
|
|
escPath, escVersion, ok := strings.Cut(mod, "@")
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
if _, err := module.UnescapePath(escPath); err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
version, err := module.UnescapeVersion(escVersion)
|
|
|
|
|
return err == nil && semver.IsValid(version)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// goSumDBCacheKey hashes the database name, its URL and the request path, as
|
|
|
|
|
// APKHandler.metadataCacheKey does, so repointing a database at another URL
|
|
|
|
|
// does not serve the old database's entries.
|
|
|
|
|
func goSumDBCacheKey(name, sumDBURL, path string) string {
|
|
|
|
|
digest := sha256.Sum256([]byte(name + "\x00" + sumDBURL + "\x00" + path))
|
|
|
|
|
return "sumdb/" + hex.EncodeToString(digest[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// serveCachedSumDBTile serves a complete tile from the metadata cache
|
2026-10-10 10:19:39 +02:00
|
|
|
// regardless of its age, since its content never changes. It reports
|
|
|
|
|
// served false when the tile is not cached, and damaged as well when the
|
|
|
|
|
// cached copy fails check.
|
|
|
|
|
func (h *GoHandler) serveCachedSumDBTile(w http.ResponseWriter, r *http.Request, cacheKey string, check func([]byte) error) (served, damaged bool) {
|
Don't let the supported check hang, and don't take a block page for an answer
Two problems with the check behind supported. The fetch it waits on is
shared with other callers and only bounded by the upstream timeout, which
defaults to 30 seconds. A firewall that silently drops packets to
sum.golang.org would stall the first go command every minute for that long,
and the go command itself has no timeout on this request. Now we wait five
seconds at most; the fetch carries on in the background, records how it
went and fills the cache for the next check. A timeout only counts as a
failure if the fetch hasn't succeeded in the meantime.
The fetch reports no error when it falls back to the cached tree after the
database failed, so the metadata result now says when it did, and the
check treats that as the database not answering. A fresh cached tree
(within metadata_ttl) still lets a timed-out check say yes, since the
fetch only falls back once upstream has failed, which a silent database
delays past our timeout. An older tree does not count: once the proxy has
lost the database, clients go there directly, as they would without the
proxy.
And the check believed any 200. Egress proxies tend to answer blocked hosts
with a 200 HTML page, which we would have cached as the signed tree and taken
as proof that the database is reachable. latest now has to start with
"go.sum database tree", or it is refused like a bad tile.
Lookups had the same gap. A block page cached as a lookup would also
replace the good copy that is served during an outage, so a lookup now has
to be a record followed by a signed tree, the way the go command reads it.
2026-10-10 11:31:01 +02:00
|
|
|
entry, body, contentType, ok := h.readCachedSumDB(r.Context(), cacheKey, nil)
|
|
|
|
|
if !ok {
|
2026-10-10 10:19:39 +02:00
|
|
|
return false, false
|
|
|
|
|
}
|
|
|
|
|
if check != nil && check(body) != nil {
|
|
|
|
|
return false, true
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
2026-10-20 15:55:33 +05:30
|
|
|
metrics.RecordMetadataCacheHit(goEcosystem, len(body))
|
2026-10-10 09:57:43 +02:00
|
|
|
|
|
|
|
|
if entry.ETag.Valid {
|
|
|
|
|
w.Header().Set(headerETag, entry.ETag.String)
|
|
|
|
|
if ifNoneMatchHits(r.Header.Get("If-None-Match"), entry.ETag.String) {
|
|
|
|
|
w.WriteHeader(http.StatusNotModified)
|
2026-10-10 10:19:39 +02:00
|
|
|
return true, false
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set(headerContentType, contentType)
|
|
|
|
|
w.Header().Set(headerContentLength, strconv.Itoa(len(body)))
|
|
|
|
|
if entry.ContentEncoding.Valid && entry.ContentEncoding.String != "" {
|
|
|
|
|
w.Header().Set(headerContentEncoding, entry.ContentEncoding.String)
|
|
|
|
|
}
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
_, _ = w.Write(body)
|
2026-10-10 10:19:39 +02:00
|
|
|
return true, false
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// proxySumDB fetches a checksum database response through the metadata
|
|
|
|
|
// cache. Unlike ProxyCached it relays the start of an upstream 404 body: the
|
2026-10-10 10:19:39 +02:00
|
|
|
// go command prints it, and it says why a module version is missing. check,
|
|
|
|
|
// when set, must accept a response before it is cached or served. damaged
|
|
|
|
|
// says the cached copy failed check.
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
func (h *GoHandler) proxySumDB(w http.ResponseWriter, r *http.Request, name, upstreamURL, cacheKey string, check func([]byte) error, damaged bool) {
|
2026-10-10 09:57:43 +02:00
|
|
|
p := h.proxy
|
|
|
|
|
if !p.CacheMetadata {
|
|
|
|
|
p.proxyMetadataStream(w, r, upstreamURL, "identity", "*/*")
|
|
|
|
|
return
|
|
|
|
|
}
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
// While the database is known to be unreachable, offline mode serves
|
|
|
|
|
// the cached copy at once rather than letting every request wait for
|
|
|
|
|
// the upstream timeout before the fetch falls back to it.
|
|
|
|
|
if h.sumDBOffline && h.sumDBProbes.recentlyFailed(name) {
|
|
|
|
|
if entry, body, contentType, ok := h.readCachedSumDB(r.Context(), cacheKey, check); ok {
|
|
|
|
|
p.writeMetadataCachedResponseWithEncoding(w, r, goEcosystem, cacheKey, body, contentType, entry.ContentEncoding.String)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
start := time.Now()
|
|
|
|
|
res := h.fetchSumDB(r.Context(), upstreamURL, cacheKey, check, damaged)
|
|
|
|
|
if res.stale {
|
|
|
|
|
// The database did not answer. Remember it here too: the go command
|
|
|
|
|
// asks supported only once per process, so a long build would lose
|
|
|
|
|
// the shortcut above a minute into an outage.
|
|
|
|
|
h.sumDBProbes.recordFailure(name, start)
|
|
|
|
|
}
|
|
|
|
|
if err := res.err; err != nil {
|
2026-10-10 09:57:43 +02:00
|
|
|
var notFound *upstreamNotFoundError
|
|
|
|
|
switch {
|
|
|
|
|
case errors.As(err, ¬Found) && len(notFound.body) > 0:
|
|
|
|
|
w.Header().Set(headerContentType, "text/plain; charset=utf-8")
|
|
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
|
w.WriteHeader(http.StatusNotFound)
|
|
|
|
|
_, _ = w.Write(notFound.body)
|
|
|
|
|
case errors.Is(err, ErrUpstreamNotFound):
|
|
|
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
|
|
|
default:
|
2026-10-15 13:35:12 +01:00
|
|
|
if writeUpstreamHeld(w, err) {
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-10-10 09:57:43 +02:00
|
|
|
p.Logger.Error("checksum database fetch failed", "url", upstreamURL, "error", err)
|
|
|
|
|
http.Error(w, "failed to fetch from upstream", http.StatusBadGateway)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
p.writeMetadataCachedResponseWithEncoding(w, r, goEcosystem, cacheKey, res.body, res.contentType, res.contentEncoding)
|
2026-10-10 09:57:43 +02:00
|
|
|
}
|
2026-10-10 10:19:39 +02:00
|
|
|
|
|
|
|
|
// fetchSumDB fetches a checksum database response through the metadata
|
|
|
|
|
// cache. A cached copy that failed check is fetched again without
|
|
|
|
|
// revalidating it: past the TTL, a database that sends an ETag would answer
|
|
|
|
|
// 304 and hand back the damaged copy for good.
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
func (h *GoHandler) fetchSumDB(ctx context.Context, upstreamURL, cacheKey string, check func([]byte) error, damaged bool) metadataResult {
|
2026-10-10 10:19:39 +02:00
|
|
|
p := h.proxy
|
|
|
|
|
if damaged {
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
return p.fetchMetadataFromUpstream(ctx, goEcosystem, cacheKey, upstreamURL, "*/*", "identity", check, nil)
|
2026-10-10 10:19:39 +02:00
|
|
|
}
|
Add upstream.go_sumdbs_offline for networks where only the proxy has egress
On a build network where clients cannot reach sum.golang.org at all, the
proxy should keep answering for the database during an outage, since a 404
on supported would only send the go command somewhere it cannot go. With
the switch set, a cached tree of any age is enough; without it, the tree
has to be fresh, as before.
That does not make every cached module verify. The go command checks a
lookup against tiles of the tree that came with it, and sum.golang.org
attaches a different tree to almost every lookup, so it can ask for a tile
nobody fetched through the proxy before. configuration.md says so.
While the database is known to be unreachable, offline mode also serves
cached lookups and tiles straight away. Otherwise each of them would sit
through the upstream timeout before the fetch falls back to the cache,
and a build would take minutes for as long as the outage lasts. A lookup
or tile that falls back to the cache renews what the proxy knows about
the database, unless a probe has reached it since the request started.
The go command asks supported only once per process, so a long build would
otherwise be back to waiting a minute into the outage. recordTimeout is
now recordFailure, since lookups use it too.
The option needs cache_metadata, since without it there is no cached tree
to fall back on, and the config refuses the combination.
2026-10-10 17:55:04 +02:00
|
|
|
return p.lookupMetadata(ctx, goEcosystem, cacheKey, upstreamURL, "*/*", "identity", check)
|
2026-10-10 10:19:39 +02:00
|
|
|
}
|