Skip to content
68 changes: 67 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The proxy never uploads artifact bytes to a scanner. Each scanner is notified wi
| Debian | Debian/Ubuntu | | ✓ |
| RPM | RHEL/Fedora | | ✓ |
| Alpine | Alpine Linux | | ✓ |
| Arch | Arch Linux | | ✗ |
| Arch | Arch Linux | | ✓ |
| Chef | Chef | | ✗ |
| Generic | Any | | ✓ |
| Helm | Kubernetes | Yes | ✓ |
Expand Down Expand Up @@ -665,6 +665,71 @@ http://localhost:8080/apk/private
apk appends the architecture and index filename to each repository line
itself.

### Arch Linux / pacman

Put the proxy at the top of `/etc/pacman.d/mirrorlist`. The default repository
name `archlinux` proxies the official GeoIP mirror
(`https://geo.mirror.pkgbuild.com`):

```
Server = http://localhost:8080/arch/archlinux/$repo/os/$arch
```

Then:

```bash
pacman -Syu
```

Repository databases (`core.db`, `core.files` and their `.tar.*` forms),
database signatures, packages and package signatures are served byte-for-byte
unchanged, so pacman's signature checks keep working. Databases use the
metadata cache (`metadata_ttl`, stale fallback). Each database refresh also
fetches its signature and stores it for exactly that database, so a signature
is only ever served with the database it signs. Enable `cache_metadata` (or set `PROXY_CACHE_METADATA=true`) so
`pacman -Sy` keeps working while the upstream is down; without it databases are
fetched fresh on every request. Packages and their `.sig` files are stored in the shared artifact
cache and remain available when the upstream is unreachable.

The proxy only starts sending a package once it has the whole file, and pacman
gives up on a download that sends nothing for 10 seconds. The fetch keeps
running and the next try is served from the cache. On a slow upstream, set
`DisableDownloadTimeout` in `/etc/pacman.conf` to avoid the failed first try.

`extra.files` is about 50 MB and is read into memory on each `pacman -F`
request. Raise `metadata_max_size` (100 MB by default) for larger custom
repositories.

A custom repository that rebuilds a package under the same version leaves the
old package and signature in the cache. Bump `pkgrel` instead, or remove the
cached version.

#### Arch Linux ARM

Arch Linux ARM has no built-in repository. Add one under `upstream.arch`, using a
regional mirror from <https://archlinuxarm.org/about/mirrors> that serves HTTPS:

```yaml
upstream:
arch:
archlinux: "https://geo.mirror.pkgbuild.com"
archlinuxarm: "https://<region>.mirror.archlinuxarm.org"
```

Setting `upstream.arch` replaces the built-in default, so keep `archlinux` in the map
if you also proxy x86_64 Arch.

Arch Linux ARM mirrors use `$arch/$repo` instead of `$repo/os/$arch`. In
`/etc/pacman.d/mirrorlist`:

```
Server = http://localhost:8080/arch/archlinuxarm/$arch/$repo
```

Avoid `mirror.archlinuxarm.org` as the upstream. It redirects each request to a
changing regional mirror over plain HTTP, so the database and the packages can come
from mirrors with a different sync state.

### GitHub Releases / mise (aqua backend)

Configure named generic upstreams:
Expand Down Expand Up @@ -993,6 +1058,7 @@ Recently cached:
| `GET /v2/*` | OCI/Docker registry protocol |
| `GET /v2/homebrew/core/*` | Homebrew core bottle manifests and blobs from GHCR |
| `GET /apk/{repository}/*` | Alpine APK repository protocol |
| `GET /arch/{repository}/*` | Arch Linux pacman repository protocol |
| `GET /generic/{name}/*` | Generic HTTP download proxy (GitHub release assets, mise/aqua) |
| `GET /debian/*` | Debian/APT repository protocol (main archive) |
| `GET /debian/{repository}/*` | Debian/APT repository protocol (named archive, e.g. security) |
Expand Down
9 changes: 9 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,15 @@ upstream:
# alpine: "https://dl-cdn.alpinelinux.org/alpine"
# private: "https://apk.example.com"

# Named Arch Linux pacman mirrors (used by /arch/{name}/).
# Defaults to {"archlinux": "https://geo.mirror.pkgbuild.com"} when empty;
# configuring any entry replaces that default. Arch Linux ARM needs a
# regional HTTPS mirror of your choice, not the mirror.archlinuxarm.org
# redirector.
# arch:
# archlinux: "https://geo.mirror.pkgbuild.com"
# archlinuxarm: "https://<region>.mirror.archlinuxarm.org"

# Named generic HTTP upstreams (used by /generic/{name}/). The remaining
# request path and query are appended to the upstream URL. GitHub release
# assets ({owner}/{repo}/releases/download/{tag}/{asset}) are cached
Expand Down
20 changes: 20 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,26 @@ verification keeps working; `.apk` packages use the shared artifact cache.
When `upstream.apk` is empty, a single repository named `alpine` pointing at
the official mirror is available; configuring any entry replaces that default.

```yaml
upstream:

# Named Arch Linux pacman mirrors, served at /arch/{name}/.
arch:
archlinux: "https://geo.mirror.pkgbuild.com"
```

Arch Linux repositories are read-only. Requests to `/arch/{name}/…` are passed
to the mirror unchanged, so both the Arch Linux layout
(`/arch/archlinux/core/os/x86_64/core.db`) and the Arch Linux ARM layout
(`/arch/archlinuxarm/aarch64/core/core.db`) work. Databases (`{repo}.db`,
`{repo}.files` and their `.tar.*` forms) are cached with the metadata TTL and
served byte-for-byte unchanged; a database's `.sig` is refreshed together with
the database. Packages and package signatures use the shared artifact cache.
When `upstream.arch` is empty, a single repository named `archlinux` pointing at
`https://geo.mirror.pkgbuild.com` is available; configuring any entry replaces
that default. Arch Linux ARM has no default: pick a regional HTTPS mirror, not
the `mirror.archlinuxarm.org` redirector.

### Debian archives

```yaml
Expand Down
9 changes: 9 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,12 @@ type UpstreamConfig struct {
// Default when empty: {"alpine": "https://dl-cdn.alpinelinux.org/alpine"}.
APK map[string]string `json:"apk" yaml:"apk"`

// Arch maps repository names to Arch Linux pacman mirror base URLs,
// served at /arch/{name}/. The remaining request path mirrors the
// upstream layout, e.g. /arch/archlinux/core/os/x86_64/core.db.
// Default when empty: {"archlinux": "https://geo.mirror.pkgbuild.com"}.
Arch map[string]string `json:"arch" yaml:"arch"`

// OCI maps names to OCI registry URLs. Requests to a named registry use
// the repository prefix upstream/{name}/, for example
// oci://proxy.example.com/upstream/ghcr/owner/chart.
Expand Down Expand Up @@ -717,6 +723,9 @@ func (u *UpstreamConfig) Validate() error {
if err := validateNamedUpstreams("upstream.apk", u.APK); err != nil {
return err
}
if err := validateNamedUpstreams("upstream.arch", u.Arch); err != nil {
return err
}
if err := validateNamedUpstreams("upstream.oci", u.OCI); err != nil {
return err
}
Expand Down
20 changes: 19 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1364,11 +1364,15 @@ func TestValidateNamedUpstreams(t *testing.T) {
wantErr bool
}{
{
name: "valid Helm, OCI, APK, and generic upstreams",
name: "valid Helm, OCI, APK, Arch, and generic upstreams",
modify: func(cfg *Config) {
cfg.Upstream.Helm = map[string]string{"bitnami": "https://charts.bitnami.com/bitnami"}
cfg.Upstream.OCI = map[string]string{"ghcr": "https://ghcr.io"}
cfg.Upstream.APK = map[string]string{"alpine": "https://dl-cdn.alpinelinux.org/alpine"}
cfg.Upstream.Arch = map[string]string{
"archlinux": "https://geo.mirror.pkgbuild.com",
"archlinuxarm": "https://de3.mirror.archlinuxarm.org",
}
cfg.Upstream.Generic = map[string]string{
"github": "https://github.com",
"github-api": "https://api.github.com",
Expand Down Expand Up @@ -1495,6 +1499,20 @@ func TestValidateNamedUpstreams(t *testing.T) {
},
wantErr: true,
},
{
name: "Arch upstream name contains path separator",
modify: func(cfg *Config) {
cfg.Upstream.Arch = map[string]string{"arch/linux": "https://geo.mirror.pkgbuild.com"}
},
wantErr: true,
},
{
name: "Arch upstream URL is not absolute",
modify: func(cfg *Config) {
cfg.Upstream.Arch = map[string]string{"archlinux": "geo.mirror.pkgbuild.com"}
},
wantErr: true,
},
{
name: "valid Debian repository",
modify: func(cfg *Config) {
Expand Down
Loading
Loading