lease

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package lease provides persistent, address-limited routes for long transfers. A lease pins setup requests and transfer bytes to one HTTP client, learns throughput as bytes arrive, and can reserve a faster route for resumption.

Index

Examples

Constants

View Source
const Direct = proxytron.Direct

Direct identifies an explicitly configured direct route.

Variables

View Source
var (
	// ErrExitInUse means the observed exit is reserved or cooling for this scope.
	ErrExitInUse = errors.New("proxytron: exit address is in use or cooling")
	// ErrExitUnknown means no configured probe returned a usable address.
	ErrExitUnknown = errors.New("proxytron: could not determine the route's exit")
	// ErrExitProbeFailed means probes work independently but not on this route.
	ErrExitProbeFailed = errors.New("proxytron: route did not pass exit probes")
	// ErrInactiveLease means the lease was released or its pool was closed.
	ErrInactiveLease = errors.New("proxytron: lease is no longer active")
)

Functions

This section is empty.

Types

type Client

type Client interface {
	ThroughProxy(string) (Client, error)
	NewRequest(context.Context, string, string, io.Reader) (*http.Request, error)
	DoOnce(*http.Request) (*http.Response, error)
	WithRoute(context.Context, string, string, func(Observation)) context.Context
	CloseIdleConnections()
}

Client adapts an application's HTTP stack to a lease. ThroughProxy must isolate cookies and connections and must never silently bypass the route. WithRoute pins subsequent requests to the client and reports observations. Its string arguments are the route ID and application scope, respectively.

type Configuration

type Configuration struct {
	Endpoints []string
	Feeds     []string
}

Configuration is immutable once published. ParseConfiguration owns its slices.

func ParseConfiguration

func ParseConfiguration(endpoints, feeds []string) (Configuration, error)

ParseConfiguration validates and deduplicates endpoints and HTTP(S) feeds. Schemeless endpoints use HTTP; SOCKS5 entries use remote destination DNS.

type ExitProber added in v0.2.0

type ExitProber struct {
	URLs    []string
	Timeout time.Duration // Per probe; nonpositive uses Detector's default.
	// IsResponseError optionally recognizes an adapter's HTTP response errors.
	// Such errors try the next URL; other I/O errors stop immediately. Standard
	// Detector response errors are recognized without this callback.
	IsResponseError func(error) bool
	// Reachable optionally checks the probes independently after all replies
	// were unusable. True confirms a route failure, which is scored once;
	// false permits proceeding with an unknown exit during a probe outage.
	// An error stops without a penalty. Nil fails with ErrExitUnknown without
	// penalizing the route. The caller chooses the diagnostic route, bounds its
	// I/O with ctx, and may cache/coalesce checks. Direct checks are never implicit.
	Reachable func(context.Context) (bool, error)
}

ExitProber confirms a lease's public address before destination work starts. URLs are tried in order, through the lease's existing client and session. It owns no clients or goroutines and is safe to share if its configuration is not mutated and its callbacks are concurrency-safe. Serialize Claim calls on any one lease, just as the destination setup using that lease is serialized.

func (ExitProber) Claim added in v0.2.0

func (p ExitProber) Claim(ctx context.Context, l *Lease) (proxytron.Egress, error)

Claim probes and atomically claims the observed exit using Lease.ClaimExit. A fresh claim is reused; a cached identity still requires confirmation. An empty URL list disables probing. A nil error with an invalid IP means probing was disabled or Reachable explicitly allowed an outage. No destination request is sent, no lease is released, and no route is silently replaced with Direct. The caller must release the lease after its work, including on a Claim error.

ProbeContext reports transport outcomes through the Client adapter; responses from the probe service do not penalize the destination scope. A confirmed unusable route returns ErrExitProbeFailed; a shared or cooling exit returns ErrExitInUse without a health penalty. Errors may also wrap the last probe error.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"

	"github.com/JohanLindvall/proxytron/lease"
)

func main() {
	// A loopback proxy supplies a synthetic IP-check response in this example.
	proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		_, _ = io.WriteString(w, "198.51.100.8")
	}))
	defer proxy.Close()
	dir, err := os.MkdirTemp("", "proxytron-example-")
	if err != nil {
		panic(err)
	}
	defer os.RemoveAll(dir)
	pool, err := lease.Open(filepath.Join(dir, "leases.db"), []string{proxy.URL}, nil, nil, nil)
	if err != nil {
		panic(err)
	}
	defer pool.Close()
	route := pool.Acquire("downloads", "", 1)
	if route == nil {
		panic("no route available")
	}
	defer route.Release()
	prober := lease.ExitProber{URLs: []string{"http://ip-check.example.test/ip"}}
	identity, err := prober.Claim(context.Background(), route)
	if err != nil {
		panic(err)
	}
	fmt.Println(identity.IP, route.Claimed())
	// Destination requests now use route.Context(ctx) until route.Release().
	

type HTTPClient

type HTTPClient struct {
	// contains filtered or unexported fields
}

HTTPClient is the standard net/http adapter. Applications with custom TLS fingerprints or challenge handling can implement Client instead.

func NewHTTPClient

func NewHTTPClient(client *http.Client) *HTTPClient

NewHTTPClient uses client for feed downloads. Nil selects a standard client.

func (*HTTPClient) CloseIdleConnections

func (c *HTTPClient) CloseIdleConnections()

CloseIdleConnections releases cached connections.

func (*HTTPClient) DoOnce

func (c *HTTPClient) DoOnce(req *http.Request) (*http.Response, error)

DoOnce performs one request, respecting a lease context and reporting the complete body read. Intentional partial closes and cancellation are ignored.

func (*HTTPClient) NewRequest

func (c *HTTPClient) NewRequest(ctx context.Context, method, url string, body io.Reader) (*http.Request, error)

NewRequest constructs a context-bound request.

func (*HTTPClient) ThroughProxy

func (c *HTTPClient) ThroughProxy(endpoint string) (Client, error)

ThroughProxy creates an isolated standard transport and cookie jar.

func (*HTTPClient) WithRoute

func (c *HTTPClient) WithRoute(ctx context.Context, _, _ string, observe func(Observation)) context.Context

WithRoute pins the standard client to the request context.

type Lease

type Lease struct {
	// contains filtered or unexported fields
}

Lease reserves one slot for a service on one route, across aliases and jobs.

func (*Lease) Address

func (l *Lease) Address() string

Address is where the route connects, without any credentials, for logs.

func (*Lease) ClaimExit

func (l *Lease) ClaimExit(exit string) bool

ClaimExit records exit as the public address the lease's route leaves by, and claims it for the lease. It refuses when other leases hold that exit for the service, as many as it allows an address, or the service's host has it cooling. The caller then gives the route back, and the pool passes over the route while that lasts.

func (*Lease) Claimed

func (l *Lease) Claimed() bool

Claimed reports whether a fresh observation has confirmed this lease's exit. A cached identity reserves capacity but still permits a fresh probe.

func (*Lease) Configured

func (l *Lease) Configured() bool

Configured checks a reserved replacement again after a live source edit. Existing requests can finish on removed routes; a not-yet-used upgrade should not start a new request there.

func (*Lease) Constrain

func (l *Lease) Constrain()

Constrain also covers transfers that finish between progress ticks: a user-imposed pause or speed ceiling must not train their EOF average.

func (*Lease) Context

func (l *Lease) Context(ctx context.Context) context.Context

Context pins requests to the reserved client and outcome observer.

func (*Lease) Cooldown

func (l *Lease) Cooldown(until time.Time)

Cooldown records an authoritative per-address host timer without treating it as a dead proxy. Other services can keep using the endpoint.

It is written on this route alone. The address's other tried ports take it up where routes are chosen (addressCooling), and those never tried may leave through another public address altogether (untested). Writing it on each port was a synced transaction per port, under the pool's lock: on a gateway of thousands, seconds in which no route could be leased.

func (*Lease) Endpoint

func (l *Lease) Endpoint() string

Endpoint returns the selected route, including credentials. Use it only to establish the connection; inventory pages already redact credentials.

func (*Lease) Exit

func (l *Lease) Exit() string

Exit is the address the route's requests were seen to leave by, once claimed; empty before then, or when no echo service answered.

func (*Lease) Failed

func (l *Lease) Failed(err error, elapsed time.Duration, transport bool)

Failed reports failures recognised above HTTP: a stalled media stream or a web page served in place of the file. Callers exclude user cancellation.

func (*Lease) ID

func (l *Lease) ID() string

ID returns a stable identifier that does not disclose credentials.

func (*Lease) Observe

func (l *Lease) Observe(o Observation)

Observe records an application's classified outcome for this lease. It does not release the lease. The caller should exclude user cancellation.

func (*Lease) ProbeContext

func (l *Lease) ProbeContext(ctx context.Context) context.Context

ProbeContext is Context for a question put to a third party through the route, such as which address it leaves by. The route answers for carrying the request: failing to, a dial or a dropped connection, counts against it as on any request, and an answer shows it alive. What the third party says back does not count, a refusal or an outage of its own, since that is not the service's host speaking about the route's address.

func (*Lease) Progress

func (l *Lease) Progress(now time.Time, bytes int64, elapsed time.Duration, limited bool) bool

Progress learns from newly written bytes, never the resumed prefix. The caller samples the file counter after its baseline has been reset. Pauses and an imposed speed ceiling reset the window rather than training the proxy to look slow. No database I/O occurs on this path. The result says a complete measurement window is ready for reconsidering the route. Setup includes CAPTCHA and ticket waits before the first bytes.

func (*Lease) Release

func (l *Lease) Release()

Release frees the reservation exactly once. Finish requests first.

func (*Lease) Upgrade

func (l *Lease) Upgrade(now time.Time, remaining int64, currentRate float64) *Lease

Upgrade reserves a proven alternative only after sustained evidence that the remaining bytes will finish sooner there, including a new ticket and failure recovery. The caller stops its old request and resumes on this lease, or releases the reservation if cancellation/completion wins. Reservation and comparison share the pool lock, so two slow transfers cannot both abandon their route in favour of the same free address.

type Observation

type Observation struct {
	Duration   time.Duration
	Bytes      int64
	Status     int
	Err        error
	RetryAfter time.Time
	Challenged bool
}

Observation describes one attempt. Applications classify challenges and authoritative retry deadlines; the pool owns scoring and cooldowns.

type Option

type Option func(*Pool)

Option configures a lease pool at Open time.

func WithChallengeClassifier

func WithChallengeClassifier(classify func(error) bool) Option

WithChallengeClassifier recognizes an application's challenge errors, including failures reported through Failed or RefusedDirect.

func WithExitTTL

func WithExitTTL(ttl time.Duration) Option

WithExitTTL sets how long a measured exit is trusted for admission decisions. Nonpositive values retain the default of one hour. Freshly claimed leases continue holding their exit until released, even after this interval.

func WithFeedFilter

func WithFeedFilter(accept func(string) bool) Option

WithFeedFilter restricts discovered routes to those the application's HTTP adapter supports. Returning false excludes the endpoint. Static endpoints should be validated by the application's configuration boundary.

func WithHealthEpoch

func WithHealthEpoch(epoch int) Option

WithHealthEpoch enables an explicit application-owned health migration. Opening a store from another epoch forgets scores and identities, retaining inventory. Omit this option to preserve health regardless of epoch metadata.

func WithPolicy

func WithPolicy(policy Policy) Option

WithPolicy installs service policy without hard-coding product names.

type Page

type Page struct {
	Rows    []Row        `json:"rows"`
	Total   int          `json:"total"`
	Offset  int          `json:"offset"`
	Limit   int          `json:"limit"`
	Summary Summary      `json:"summary"`
	Sources []SourceView `json:"sources"`
}

Page reports a bounded inventory view and aggregate counts.

func EmptyPage

func EmptyPage(q Query) Page

EmptyPage returns an empty view with a bounded page size.

type Policy

type Policy struct {
	RecoveryScopes []string
	CappedScopes   []string
}

Policy identifies caller-defined service scopes. RecoveryScopes may borrow measurements from other scopes. Bandwidth from CappedScopes is excluded because a service's own limit does not measure the proxy's capacity.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool owns the Bolt database and never stores active leases. A restart may reuse an idle route, but must still honour its persisted host cooldown.

func Open

func Open(path string, endpoints, feeds []string, client Client, log *slog.Logger, opts ...Option) (*Pool, error)

Open restores inventory before any network request. Invalid configuration and a locked/corrupt database fail once at startup, without replacing it.

func (*Pool) Acquire

func (p *Pool) Acquire(site, preferred string, files int) *Lease

Acquire never waits or writes to disk. A preferred route lets a paused or interrupted file reuse its unexpired, IP-bound ticket when it is healthy.

func (*Pool) AcquireFor

func (p *Pool) AcquireFor(site, preferred string, files int, remaining int64, waf bool) *Lease

AcquireFor ranks routes for the bytes this file still needs. A short remaining tail values setup latency more than a long transfer does. Selection and reservation share one lock: unrated and preferred routes obey the same per-address limit, starting before the first request.

func (*Pool) Close

func (p *Pool) Close() error

Close is called after workers release their leases.

func (*Pool) Configure

func (p *Pool) Configure(c Configuration, discovery bool)

Configure publishes a validated configuration without disk or network I/O. Existing leases keep their client even when their endpoint is removed. The background refresh persists newly added inventory and fetches new feeds.

func (*Pool) DetectEgress

func (p *Pool) DetectEgress(ctx context.Context, d proxytron.Detector, endpoint string) (proxytron.Egress, error)

DetectEgress records the real exit IP and groups leases and service cooldowns by that address. Different gateways can share an exit. Detection runs outside the pool lock; a failed check preserves the previous identity. For rotating gateways the caller should check again when its sticky session changes.

func (*Pool) Egress

func (p *Pool) Egress(endpoint string) (proxytron.Egress, bool)

Egress returns the last measured identity, false when it has never been checked.

func (*Pool) Page

func (p *Pool) Page(site string, q Query) Page

Page reports recent measurements and evaluates the selector's score at the mean success probability. Reading the UI never draws from the selector's random source or changes its priors. Endpoint credentials are redacted.

func (*Pool) Refresh

func (p *Pool) Refresh(ctx context.Context)

Refresh retains surviving health and each failed source's last good list. Routine attempts are a day apart, including across restarts or failures. Earlier top-ups require runnable demand and too few usable, scored routes, and are at least five minutes apart. Network I/O holds neither the pool lock nor a Bolt transaction.

func (*Pool) RefusedDirect

func (p *Pool) RefusedDirect(site string, err error, elapsed time.Duration)

RefusedDirect records a challenge encountered before a file opted into routing. The pool must not immediately select the same refused address. No lease was held, so this observation does not alter active counts.

func (*Pool) SetDemand

func (p *Pool) SetDemand(routes map[string]int)

SetDemand publishes every service's runnable route demand atomically, so discovery cannot act on a mix of old and new concurrency allocations. An empty map suppresses shortage-driven refreshes while idle or paused. Demand is not persisted: restoring a pool does not mean a queue is ready.

func (*Pool) Start

func (p *Pool) Start(ctx context.Context)

Start refreshes in the background. Restored routes can be used immediately; slow or unavailable feeds do not delay startup or other download hosts.

type Query

type Query struct {
	Offset, Limit        int
	Search, Status, Sort string
	Site                 string
}

Query bounds the independent inventory endpoint. Large feeds never ride along with queue snapshots, and sorting/filtering happens on the server.

type Row

type Row struct {
	ID            string    `json:"id"`
	URL           string    `json:"url"`
	Source        string    `json:"source"`
	Status        string    `json:"status"`
	Score         float64   `json:"score"`
	Throughput    float64   `json:"throughput"`
	CurrentSpeed  float64   `json:"currentSpeed"`
	SuccessRate   float64   `json:"successRate"`
	Requests      int       `json:"requests"`
	WAFRequests   int       `json:"wafRequests,omitempty"`
	WAFSuccess    float64   `json:"wafSuccess,omitempty"`
	Active        int       `json:"active"`
	CooldownUntil time.Time `json:"cooldownUntil,omitzero"`
	LastSuccess   time.Time `json:"lastSuccess,omitzero"`
}

Row reports a credential-redacted inventory entry and its measurements.

type SourceView

type SourceView struct {
	URL        string    `json:"url"`
	Count      int       `json:"count"`
	Fetched    time.Time `json:"fetched,omitzero"`
	Refreshing bool      `json:"refreshing"`
	Failed     bool      `json:"failed"`
}

SourceView reports cached feed membership and refresh state.

type Summary

type Summary struct {
	Total     int `json:"total"`
	Available int `json:"available"`
	Active    int `json:"active"`
	Cooling   int `json:"cooling"`
	Untested  int `json:"untested"`
}

Summary reports aggregate route availability.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL