mirror of
https://github.com/hanzoai/proxy.git
synced 2026-10-30 18:23:27 +00:00
Pool braided three independent things into one struct — what an upstream can
serve, how to reach it, and how it has been doing — and Dial braided four more:
fencing, selection, failover, transport. Every new policy meant another field
and another branch inside a method nobody outside the package could extend.
There is one type now:
type Exit func(ctx, Need, addr) (net.Conn, error)
A vendor, our own egress, a test double and the composition of a dozen of them
are all that, so the rest of the package is functions that return one. Rule is
Exit to Exit and holds fencing and capability; Order is a ranking over a set and
holds health and session pinning; Try walks an order; Chain composes. A vendor
stops being behaviour and becomes a Gate, which is data with an Exit() on it.
What this buys is that a policy no longer has to be anticipated. Fence wraps a
whole route, so an exit added later cannot miss it — it could before, being a
per-Dial check on a struct anyone could append to. Health hands back its Order
and the Rule that feeds it as a pair, because it is one fact observed in two
places and returning only the Order left the counting to be wired by hand.
Chain is generic: composing endomorphisms is not a fact about proxies.
Co-authored-by: Hanzo Dev <[email protected]>
86 lines
2.9 KiB
Go
86 lines
2.9 KiB
Go
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
)
|
|
|
|
// ErrClosed means the target is inside the fence rather than out on the web.
|
|
//
|
|
// This is the difference between an agentic scraping platform and a way into our
|
|
// own cluster. A guest names a URL, which is the job — but a URL can name
|
|
// 10.0.0.144:6443, or the metadata address that hands out credentials. Neither
|
|
// is the web, and a proxy that dials them is a hole with an HTTP interface.
|
|
var ErrClosed = errors.New("destination is not public")
|
|
|
|
// Fence refuses destinations that are not out on the web. A Rule, so it applies
|
|
// once around whatever it wraps and every route underneath inherits it —
|
|
// there is no exit that can be added later and quietly miss the check.
|
|
func Fence(ok func(addr string) error) Rule {
|
|
return func(e Exit) Exit {
|
|
return func(ctx context.Context, n Need, addr string) (net.Conn, error) {
|
|
if err := ok(addr); err != nil {
|
|
return nil, err
|
|
}
|
|
return e(ctx, n, addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Anywhere places no fence. Naming it is the point: there is no flag that
|
|
// quietly does this, so removing the fence appears in the code that removed it.
|
|
func Anywhere(string) error { return nil }
|
|
|
|
// cgnat is carrier-grade NAT, which netip does not count as private and which is
|
|
// where a good deal of private infrastructure actually lives.
|
|
var cgnat = netip.MustParsePrefix("100.64.0.0/10")
|
|
|
|
// inside names that can only resolve to something of ours. A single-label name
|
|
// is included because it resolves through the resolver's search domains, which
|
|
// on a cluster node means straight into the cluster.
|
|
var inside = []string{".local", ".internal", ".localdomain", ".svc", ".cluster.local", ".arpa"}
|
|
|
|
// Public reports whether addr is somewhere out on the web.
|
|
//
|
|
// Deliberately a check on what was ASKED for rather than on what the name
|
|
// resolves to. Resolving here would leak every target to our own resolver and
|
|
// answer the wrong question: the exit resolves from where the exit stands.
|
|
func Public(addr string) error {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %q is not host:port", ErrClosed, addr)
|
|
}
|
|
a, err := netip.ParseAddr(strings.Trim(host, "[]"))
|
|
if err != nil {
|
|
return publicName(host)
|
|
}
|
|
a = a.Unmap()
|
|
switch {
|
|
case a.IsLoopback(), a.IsPrivate(), a.IsUnspecified(), a.IsMulticast(),
|
|
a.IsLinkLocalUnicast(), a.IsLinkLocalMulticast(), a.IsInterfaceLocalMulticast(),
|
|
cgnat.Contains(a):
|
|
return fmt.Errorf("%w: %s", ErrClosed, a)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func publicName(host string) error {
|
|
h := strings.ToLower(strings.TrimSuffix(host, "."))
|
|
if h == "" {
|
|
return fmt.Errorf("%w: empty host", ErrClosed)
|
|
}
|
|
if !strings.Contains(h, ".") {
|
|
return fmt.Errorf("%w: %q has no public suffix", ErrClosed, host)
|
|
}
|
|
for _, s := range inside {
|
|
if strings.HasSuffix(h, s) {
|
|
return fmt.Errorf("%w: %q", ErrClosed, host)
|
|
}
|
|
}
|
|
return nil
|
|
}
|