feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
924f111e48
commit
f8e0520817
197 files changed
+18655
-1416
No files matched your search
@@ -0,0 +1,137 @@
|
||||
package pipeline
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/httpclient"
|
||||
)
|
||||
|
||||
type cdnRoundTripper struct {
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
func (t *cdnRoundTripper) CloseIdleConnections() { t.transport.CloseIdleConnections() }
|
||||
|
||||
func (t *cdnRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if _, err := ValidateCDNURL(req.URL.String()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := t.transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Transport has already decoded gzip here when it was negotiated by net/http.
|
||||
resp.Body = &limitedCDNBody{ReadCloser: resp.Body, remaining: cdnBodyLimit}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type cdnLookupIP func(context.Context, string) ([]net.IP, error)
|
||||
type cdnDialContext func(context.Context, string, string) (net.Conn, error)
|
||||
|
||||
// cdnDial resolves every new connection and dials only an address from that checked answer.
|
||||
// A mixed public/private DNS answer is rejected in full, regardless of address ordering.
|
||||
func cdnDial(ctx context.Context, network, address string, lookup cdnLookupIP, dial cdnDialContext) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pipeline: cdn dial address: %w", err)
|
||||
}
|
||||
if isBlockedCDNHostname(host) {
|
||||
return nil, fmt.Errorf("pipeline: cdn url blocked host")
|
||||
}
|
||||
var ips []net.IP
|
||||
if addr, err := netip.ParseAddr(host); err == nil {
|
||||
ips = []net.IP{net.IP(addr.AsSlice())}
|
||||
} else {
|
||||
resolveCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
ips, err = lookup(resolveCtx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pipeline: cdn url dns lookup: %w", err)
|
||||
}
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("pipeline: cdn url dns lookup: no addresses")
|
||||
}
|
||||
checked := make([]netip.Addr, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
addr, ok := netip.AddrFromSlice(ip)
|
||||
if !ok || isBlockedCDNIP(addr) {
|
||||
return nil, fmt.Errorf("pipeline: cdn url resolves to blocked address")
|
||||
}
|
||||
checked = append(checked, addr.Unmap())
|
||||
}
|
||||
var dialErr error
|
||||
for _, addr := range checked {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := dial(ctx, network, net.JoinHostPort(addr.String(), port))
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
dialErr = err
|
||||
}
|
||||
return nil, fmt.Errorf("pipeline: cdn dial: %w", dialErr)
|
||||
}
|
||||
|
||||
func secureCDNClient(hc *http.Client) (*http.Client, error) {
|
||||
if hc == nil {
|
||||
hc = httpclient.New(httpclient.DefaultTimeout)
|
||||
}
|
||||
client := *hc
|
||||
if pools, ok := client.Transport.(*pipelineTransport); ok {
|
||||
client.Transport = pools.cdn
|
||||
}
|
||||
if client.Timeout <= 0 {
|
||||
client.Timeout = httpclient.DefaultTimeout
|
||||
}
|
||||
if _, ok := client.Transport.(*cdnRoundTripper); !ok {
|
||||
base := http.DefaultTransport.(*http.Transport).Clone()
|
||||
switch tr := client.Transport.(type) {
|
||||
case nil:
|
||||
case *http.Transport:
|
||||
// Keep the caller's trust roots for TLS test servers. Other transport
|
||||
// hooks (including custom protocols and dialers) are not inherited.
|
||||
if tr.TLSClientConfig != nil {
|
||||
base.TLSClientConfig = tr.TLSClientConfig.Clone()
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("pipeline: cdn client requires *http.Transport")
|
||||
}
|
||||
// A proxy connects to the destination outside this process, where its DNS answer
|
||||
// cannot be checked. CDN requests therefore always connect directly.
|
||||
base.Proxy = nil
|
||||
base.DialTLSContext = nil
|
||||
base.DialTLS = nil
|
||||
base.TLSNextProto = nil
|
||||
dialer := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
base.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return cdnDial(ctx, network, address, func(ctx context.Context, host string) ([]net.IP, error) {
|
||||
return net.DefaultResolver.LookupIP(ctx, "ip", host)
|
||||
}, dialer.DialContext)
|
||||
}
|
||||
client.Transport = &cdnRoundTripper{transport: base}
|
||||
}
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if _, err := ValidateCDNURL(req.URL.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(via) >= 10 {
|
||||
return fmt.Errorf("pipeline: too many cdn redirects")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
// NewCDNHTTPClient returns a reusable HTTPS client that pins each CDN connection
|
||||
// to a checked DNS answer. Environment HTTP proxies are intentionally ignored.
|
||||
func NewCDNHTTPClient() *http.Client {
|
||||
client, _ := secureCDNClient(httpclient.New(httpclient.DefaultTimeout))
|
||||
return client
|
||||
}
|
||||
Reference in new issue
Block a user