Documentation
¶
Overview ¶
Package trafficpolicy holds the per-domain traffic control model (headers, path forwarders, geolocation, caching, redirects): the wire types the API, CLI and dashboard share, their validation, and a human readable preview. It has no dependencies outside the standard library so the ingress, store and API layers can all import it.
Index ¶
- Constants
- Variables
- func BotAgentsFor(l Limits) ([]string, error)
- func CanonicalPlan(domain, preset string) (from, to string, err error)
- func CheckRedirectLoop(rows map[string]HostRedirect, from, toHost string) error
- func Counterpart(domain string) (string, error)
- func DefaultBotAgents() []string
- func ForwardedPath(f Forwarder, path string) string
- func IsCountryCode(c string) bool
- func IsKind(k string) bool
- func IsToken(s string) bool
- func NeverGeoBlocked(a netip.Addr) bool
- func ParseBotList(text string) []string
- func ParseIPOrPrefix(s string) (netip.Prefix, error)
- func PrefixMatches(prefix, path string) bool
- func RedirectTarget(target string, req *url.URL) string
- func SafeHeaderValue(v string) bool
- func TargetHost(target string) string
- func ValidOrigin(o string) bool
- type BotMatcher
- type CORSPreset
- type Cache
- type CacheRule
- type Context
- type EgressGuard
- type FieldError
- type Forwarder
- type Forwarders
- type Geo
- type GeoDecision
- type HeaderRule
- type Headers
- type Hop
- type HopEnv
- type HostRedirect
- type IPLimiter
- type Limits
- type Match
- type Policy
- type Redirects
- type ResolvedTarget
- type Resolver
- type SecurityPreset
- type Shield
- type ShieldEffective
- type ShieldReason
- type Step
- type ValidationError
Constants ¶
const ( PresetWWWToApex = "www-to-apex" PresetApexToWWW = "apex-to-www" PresetServeBoth = "both" )
Canonical host presets.
const ( ActionApp = "app" ActionURL = "url" ActionRedirect = "redirect" )
Forwarder actions.
const ( HostPreserve = "preserve" HostUpstream = "upstream" HostCustom = "custom" )
Host header modes for a forwarder.
const ( GeoAllow = "allow" GeoDeny = "deny" GeoActionBlock = "block" GeoActionRedirect = "redirect" GeoActionErrorPage = "error_page" GeoUnknownAllow = "allow" GeoUnknownBlock = "block" )
Geo modes, actions and unknown-country handling.
const ( SideRequest = "request" SideResponse = "response" OpSet = "set" OpAdd = "add" OpRemove = "remove" )
Header rule sides and operations.
const ( EnvMaxHeaderRules = "APP_DOMAIN_POLICY_MAX_HEADER_RULES" EnvMaxForwarders = "APP_DOMAIN_POLICY_MAX_FORWARDERS" EnvMaxCacheRules = "APP_DOMAIN_POLICY_MAX_CACHE_RULES" EnvMaxRegexLen = "APP_DOMAIN_POLICY_MAX_REGEX_LEN" EnvMaxHeaderValueLen = "APP_DOMAIN_POLICY_MAX_HEADER_VALUE_LEN" EnvMaxExemptEntries = "APP_DOMAIN_POLICY_MAX_GEO_EXEMPT" EnvForwardTimeout = "APP_FORWARDER_TIMEOUT" EnvForwardMaxTimeout = "APP_FORWARDER_MAX_TIMEOUT" EnvForwardDialTimeout = "APP_FORWARDER_DIAL_TIMEOUT" EnvCacheMaxObject = "APP_INGRESS_CACHE_MAX_OBJECT_BYTES" EnvCacheMaxTTL = "APP_INGRESS_CACHE_MAX_TTL" EnvMaxErrorBody = "APP_DOMAIN_POLICY_MAX_BODY_BYTES" EnvShieldMaxTrackedIPs = "APP_SHIELD_MAX_TRACKED_IPS" EnvShieldRetryAfter = "APP_SHIELD_RETRY_AFTER_SECONDS" EnvShieldMaxRetryAfter = "APP_SHIELD_MAX_RETRY_AFTER_SECONDS" EnvShieldTarpitMax = "APP_SHIELD_TARPIT_MAX" EnvShieldTarpitSlots = "APP_SHIELD_TARPIT_SLOTS" EnvShieldMaxBotAgents = "APP_SHIELD_MAX_BOT_AGENTS" EnvShieldAttackRate = "APP_SHIELD_ATTACK_RATE_PER_MINUTE" EnvShieldAttackBurst = "APP_SHIELD_ATTACK_BURST" EnvShieldAttackConcurrent = "APP_SHIELD_ATTACK_MAX_CONCURRENT" EnvShieldAttackMax = "APP_SHIELD_ATTACK_MAX_DURATION" EnvShieldAttackDefault = "APP_SHIELD_ATTACK_DEFAULT_DURATION" EnvShieldBotListFile = "APP_SHIELD_BOT_LIST_FILE" EnvShieldRecommendedRate = "APP_SHIELD_RECOMMENDED_RATE_PER_MINUTE" EnvShieldRecommendedBurst = "APP_SHIELD_RECOMMENDED_BURST" EnvShieldRecommendedConc = "APP_SHIELD_RECOMMENDED_MAX_CONCURRENT" )
Environment variables that tune the caps below. Every one is optional.
const ( MatchPrefix = "prefix" MatchExact = "exact" MatchRegex = "regex" )
Path match kinds.
const ( EnvForwardAllowPrivate = "APP_FORWARDER_ALLOW_PRIVATE_NETWORKS" EnvForwardAllowLoopback = "APP_FORWARDER_ALLOW_LOOPBACK" )
Environment switches that let forwarders reach non-public addresses.
const ( KindHeaders = "headers" KindForwarders = "forwarders" KindGeo = "geo" KindCache = "cache" KindRedirects = "redirects" )
Policy kinds, one stored row each per domain.
const ( SlashOff = "off" SlashAdd = "add" SlashRemove = "remove" )
Trailing slash normalisation modes.
const DefaultHSTSMaxAge = 31536000
DefaultHSTSMaxAge is one year, the preload list minimum.
const KindShield = "shield"
KindShield is the per-domain bot and abuse defense section.
const MaxHops = 8
MaxHops bounds a simulated chain; longer chains are reported as loops.
Variables ¶
AliasStatuses are the redirect codes an alias or canonical redirect may use.
var ErrBlockedTarget = errors.New("trafficpolicy: forward target address is not allowed")
ErrBlockedTarget is returned when a forwarder URL resolves to an address the egress guard refuses.
var Kinds = []string{KindShield, KindRedirects, KindGeo, KindHeaders, KindCache, KindForwarders}
Kinds lists every policy kind in evaluation order.
Functions ¶
func BotAgentsFor ¶
BotAgentsFor returns the shipped denylist, or the file named by l.ShieldBotListFile when set.
func CanonicalPlan ¶
CanonicalPlan returns the redirect a preset installs: requests to from go to to. Serve both returns two empty strings.
func CheckRedirectLoop ¶
func CheckRedirectLoop(rows map[string]HostRedirect, from, toHost string) error
CheckRedirectLoop reports whether adding from -> toHost to rows would make a chain that returns to from, or exceeds MaxHops.
func Counterpart ¶
Counterpart returns the www or apex twin of domain.
func DefaultBotAgents ¶
func DefaultBotAgents() []string
DefaultBotAgents returns the shipped denylist.
func ForwardedPath ¶
ForwardedPath is the path the upstream sees after strip and rewrite.
func IsCountryCode ¶
IsCountryCode reports whether c is an assigned ISO 3166-1 alpha-2 code.
func NeverGeoBlocked ¶
NeverGeoBlocked reports addresses geo rules never apply to: private, loopback, link-local and unspecified addresses have no country.
func ParseBotList ¶
ParseBotList reads one substring per line, skipping blanks and # comments.
func ParseIPOrPrefix ¶
ParseIPOrPrefix accepts "203.0.113.7" or "203.0.113.0/24".
func PrefixMatches ¶
PrefixMatches is segment aware: /api matches /api and /api/x, not /apix.
func RedirectTarget ¶
RedirectTarget mirrors the ingress: a bare origin target keeps the request's path and query, a target with its own path is used as is.
func SafeHeaderValue ¶
SafeHeaderValue reports whether v can be written into a header without splitting it: no CR, LF or NUL, and no other control byte except tab.
func TargetHost ¶
TargetHost returns the lower case host of a redirect target URL.
func ValidOrigin ¶
ValidOrigin reports whether o is a bare http(s) origin.
Types ¶
type BotMatcher ¶
type BotMatcher struct {
// contains filtered or unexported fields
}
BotMatcher matches user agents against lower-case substrings.
func NewBotMatcher ¶
func NewBotMatcher(l Limits, extra []string) (*BotMatcher, error)
NewBotMatcher builds a matcher over BotAgentsFor plus extra per-domain entries.
func NewBotMatcherFrom ¶
func NewBotMatcherFrom(patterns []string) *BotMatcher
NewBotMatcherFrom builds a matcher over exactly patterns.
func (*BotMatcher) Match ¶
func (m *BotMatcher) Match(userAgent string) bool
Match reports whether userAgent contains a denylisted substring.
type CORSPreset ¶
type CORSPreset struct {
Origins []string `json:"origins"`
Methods []string `json:"methods,omitempty"`
Headers []string `json:"headers,omitempty"`
ExposeHeaders []string `json:"expose_headers,omitempty"`
Credentials bool `json:"credentials,omitempty"`
MaxAgeSeconds int `json:"max_age_seconds,omitempty"`
// Preflight answers OPTIONS preflight requests at the ingress instead of
// passing them to the app.
Preflight bool `json:"preflight"`
}
CORSPreset answers cross origin requests from Origins at the ingress.
type Cache ¶
type Cache struct {
Enabled bool `json:"enabled"`
Rules []CacheRule `json:"rules"`
MaxObjectBytes int64 `json:"max_object_bytes,omitempty"`
}
Cache is a domain's cache policy.
type CacheRule ¶
type CacheRule struct {
Match Match `json:"match"`
// TTLSeconds applies when the app sends no max-age or s-maxage, or
// always when OverrideUpstream is set.
TTLSeconds int `json:"ttl_seconds"`
OverrideUpstream bool `json:"override_upstream,omitempty"`
// StaleWhileRevalidateSeconds serves an expired entry while one
// background request refreshes it.
StaleWhileRevalidateSeconds int `json:"stale_while_revalidate_seconds,omitempty"`
// StatusCodes cached; empty means 200 only.
StatusCodes []int `json:"status_codes,omitempty"`
// Vary lists request headers that split the cache key.
Vary []string `json:"vary,omitempty"`
// CacheWithCookies keeps caching when the request carries a Cookie.
// Off by default: a cookie usually means a personalised page.
CacheWithCookies bool `json:"cache_with_cookies,omitempty"`
// Never marks paths that must not be cached: the first matching rule
// wins, so list these before broader cache rules.
Never bool `json:"never,omitempty"`
// StaleIfErrorSeconds serves an expired entry, marked STALE, when the
// app answers 5xx or cannot be reached.
StaleIfErrorSeconds int `json:"stale_if_error_seconds,omitempty"`
// IgnoreQueryParams are dropped from the cache key (tracking params).
IgnoreQueryParams []string `json:"ignore_query_params,omitempty"`
// KeyCookies adds these cookies' values to the cache key; requires
// CacheWithCookies.
KeyCookies []string `json:"key_cookies,omitempty"`
}
CacheRule selects responses to keep in the ingress cache.
func StaticAssetsPreset ¶
func StaticAssetsPreset() []CacheRule
StaticAssetsPreset caches common static file types for an hour when the app sends no Cache-Control of its own; an app's max-age or s-maxage wins, so fingerprinted assets sent as immutable keep their long lifetime.
func (CacheRule) EffectiveStatusCodes ¶
EffectiveStatusCodes returns the statuses the rule stores.
type Context ¶
type Context struct {
// Domain is the host the policy belongs to, lower case.
Domain string
// TLSReal is true when browsers see a publicly trusted certificate for
// Domain (ACME, an uploaded certificate, or TLS terminated upstream).
TLSReal bool
// AppExists reports whether a forwarder's target app exists. Nil skips
// the check.
AppExists func(name string) bool
}
Context is what validation needs to know about the domain and instance.
type EgressGuard ¶
EgressGuard decides which addresses an external forwarder may reach. Metadata and link-local ranges are refused even when private is allowed, mirroring platformimport.NetworkPolicy.
func EgressGuardFromEnv ¶
func EgressGuardFromEnv(getenv func(string) string) EgressGuard
EgressGuardFromEnv reads the two switches above.
func (EgressGuard) Check ¶
func (g EgressGuard) Check(addr netip.Addr) error
Check reports whether addr may be dialed.
func (EgressGuard) CheckTarget ¶
func (g EgressGuard) CheckTarget(ctx context.Context, r Resolver, rawURL string) (ResolvedTarget, error)
CheckTarget resolves rawURL and checks every address it resolves to, so a name with one public and one private record is refused as a whole.
type FieldError ¶
FieldError is one validation failure, addressed by a dotted field path (for example "rules[2].name") so the dashboard can show it inline.
type Forwarder ¶
type Forwarder struct {
Name string `json:"name,omitempty"`
Match Match `json:"match"`
Action string `json:"action"`
App string `json:"app,omitempty"`
URL string `json:"url,omitempty"`
RedirectStatus int `json:"redirect_status,omitempty"`
// StripPrefix removes Match.Path before forwarding (prefix rules only).
StripPrefix bool `json:"strip_prefix,omitempty"`
// RewritePrefix is prepended to the (possibly stripped) path.
RewritePrefix string `json:"rewrite_prefix,omitempty"`
Host string `json:"host,omitempty"`
HostValue string `json:"host_value,omitempty"`
// WebSocket nil or true passes Upgrade requests through.
WebSocket *bool `json:"websocket,omitempty"`
TimeoutSeconds int `json:"timeout_seconds,omitempty"`
}
Forwarder is one ordered path rule. The first matching rule wins; a request no rule matches goes to the domain's own app.
func (Forwarder) HostMode ¶
HostMode returns the effective Host header mode: external URLs default to the upstream's own host, apps keep the visitor's.
func (Forwarder) WebSocketEnabled ¶
WebSocketEnabled reports whether Upgrade requests pass through.
type Forwarders ¶
type Forwarders struct {
Rules []Forwarder `json:"rules"`
}
Forwarders is a domain's ordered path rules.
type Geo ¶
type Geo struct {
Mode string `json:"mode"`
Countries []string `json:"countries"`
Action string `json:"action"`
RedirectURL string `json:"redirect_url,omitempty"`
StatusCode int `json:"status_code,omitempty"`
Body string `json:"body,omitempty"`
// Exempt addresses and ranges are never blocked by country.
Exempt []string `json:"exempt,omitempty"`
// Unknown decides requests whose country cannot be determined.
Unknown string `json:"unknown,omitempty"`
}
Geo is a domain's country access rule.
func (Geo) BlockStatus ¶
BlockStatus returns the status code for the block and error page actions.
func (Geo) Decide ¶
Decide evaluates g for a client address and its resolved country ("" when unknown). exempt is g.Exempt parsed once by the caller.
func (Geo) UnknownMode ¶
UnknownMode returns the effective unknown-country handling (allow by default).
type GeoDecision ¶
GeoDecision is the outcome of evaluating a Geo rule for one request.
type HeaderRule ¶
type HeaderRule struct {
Side string `json:"side"`
Op string `json:"op"`
Name string `json:"name"`
Value string `json:"value,omitempty"`
}
HeaderRule is one ordered header change. Response rules are applied after the app answered, so they win over the app's own value.
type Headers ¶
type Headers struct {
Rules []HeaderRule `json:"rules"`
Security *SecurityPreset `json:"security,omitempty"`
CORS *CORSPreset `json:"cors,omitempty"`
// HideServer removes Server and X-Powered-By from every response.
HideServer bool `json:"hide_server,omitempty"`
// ForwardedPrefix sets X-Forwarded-Prefix on requests, the only
// X-Forwarded-* header an operator may set.
ForwardedPrefix string `json:"forwarded_prefix,omitempty"`
}
Headers is a domain's header policy.
type Hop ¶
type Hop struct {
URL string `json:"url"`
Status int `json:"status"`
Location string `json:"location,omitempty"`
Note string `json:"note"`
}
Hop is one response in a redirect chain.
type HopEnv ¶
type HopEnv struct {
// Hosts routed by this instance, lower case.
Hosts map[string]bool
// Redirects maps a host to its normalisation settings.
Redirects map[string]Redirects
// HostRedirects maps a host to its whole-host redirect.
HostRedirects map[string]HostRedirect
// Maintenance hosts answer 503 before any redirect.
Maintenance map[string]bool
// AutoHTTPS is true when the ingress itself redirects every http
// request to https (the HTTP listener is bound and TLS ends here).
AutoHTTPS bool
// Terminated is true when a proxy in front terminates TLS; force HTTPS
// then trusts its X-Forwarded-Proto.
Terminated bool
// HTTPSPort is the public HTTPS port; 0 or 443 is omitted from URLs.
HTTPSPort int
}
HopEnv is everything the redirect simulation needs about the instance.
type HostRedirect ¶
type HostRedirect struct {
TargetURL string `json:"target_url"`
StatusCode int `json:"status_code"`
}
HostRedirect is one domain_redirect row: every request to the host is sent to TargetURL, keeping path and query when TargetURL is a bare origin.
type IPLimiter ¶
type IPLimiter struct {
// contains filtered or unexported fields
}
IPLimiter is a per-client-IP token bucket and concurrency counter. State lives only in memory and is capped: the least recently seen address is evicted first, so a flood of distinct addresses cannot grow it.
func NewIPLimiter ¶
NewIPLimiter tracks at most capacity addresses. A nil now uses time.Now.
func (*IPLimiter) Acquire ¶
func (l *IPLimiter) Acquire(addr netip.Addr, ratePerMinute, burst, maxConcurrent int) (release func(), reason ShieldReason, ok bool)
Acquire admits one request from addr, or reports why not. On success the caller must call release when the request finishes. ratePerMinute 0 skips the rate check and maxConcurrent 0 skips the concurrency check.
type Limits ¶
type Limits struct {
MaxHeaderRules int
MaxForwarders int
MaxCacheRules int
MaxRegexLen int
MaxHeaderValueLen int
MaxListEntries int
MaxExemptEntries int
MaxBodyBytes int
ForwardTimeout time.Duration
ForwardMaxTimeout time.Duration
ForwardDialTimeout time.Duration
CacheMaxObject int64
CacheMaxTTL time.Duration
ShieldMaxTrackedIPs int
ShieldRetryAfter int
ShieldMaxRetryAfter int
ShieldTarpitMax time.Duration
ShieldTarpitSlots int
ShieldMaxBotAgents int
ShieldAttackRate int
ShieldAttackBurst int
ShieldAttackConcurrent int
ShieldAttackMax time.Duration
ShieldAttackDefault time.Duration
ShieldBotListFile string
ShieldRecommendedRate int
ShieldRecommendedBurst int
ShieldRecommendedConcurrent int
}
Limits caps the size of every policy section. Built from the environment with LimitsFromEnv; DefaultLimits is the zero-config value.
func DefaultLimits ¶
func DefaultLimits() Limits
DefaultLimits are the caps used when no environment override is set.
func LimitsFromEnv ¶
LimitsFromEnv overlays any set environment variable on DefaultLimits. Unparseable or non-positive values keep the default.
type Match ¶
type Match struct {
Kind string `json:"kind"`
Path string `json:"path"`
Methods []string `json:"methods,omitempty"`
}
Match selects requests by path and, optionally, method.
type Policy ¶
type Policy struct {
Headers *Headers `json:"headers,omitempty"`
Forwarders *Forwarders `json:"forwarders,omitempty"`
Geo *Geo `json:"geo,omitempty"`
Cache *Cache `json:"cache,omitempty"`
Redirects *Redirects `json:"redirects,omitempty"`
Shield *Shield `json:"shield,omitempty"`
}
Policy is every traffic control configured for one domain. A nil section means the domain uses the default behavior for it.
type Redirects ¶
type Redirects struct {
ForceHTTPS bool `json:"force_https"`
// ForceHTTPSStatus is 308 (default, keeps the method) or 301.
ForceHTTPSStatus int `json:"force_https_status,omitempty"`
TrailingSlash string `json:"trailing_slash,omitempty"`
LowercaseHost bool `json:"lowercase_host,omitempty"`
}
Redirects is a domain's request normalisation: force HTTPS, trailing slash and lower case host. Canonical host and alias redirects live in the domain_redirect table, one row per redirected host.
func (Redirects) HTTPSStatus ¶
HTTPSStatus returns the effective force HTTPS status.
type ResolvedTarget ¶
type ResolvedTarget struct {
Scheme string
Host string
Port string
Addr netip.Addr
BasePath string
ServerName string
}
ResolvedTarget is an external forwarder URL pinned to one checked address.
func (ResolvedTarget) Dial ¶
func (t ResolvedTarget) Dial() string
Dial is the ip:port Caddy should connect to.
type Resolver ¶
type Resolver interface {
LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)
}
Resolver is the DNS surface CheckTarget needs; *net.Resolver satisfies it.
type SecurityPreset ¶
type SecurityPreset struct {
// HSTS needs a publicly trusted certificate; validation refuses it otherwise.
HSTS bool `json:"hsts"`
HSTSMaxAge int `json:"hsts_max_age,omitempty"`
HSTSIncludeSubdomains bool `json:"hsts_include_subdomains,omitempty"`
ContentTypeOptions bool `json:"content_type_options"`
FrameOptions string `json:"frame_options,omitempty"`
ReferrerPolicy string `json:"referrer_policy,omitempty"`
PermissionsPolicy string `json:"permissions_policy,omitempty"`
}
SecurityPreset is the one click set of browser hardening headers.
func DefaultSecurityPreset ¶
func DefaultSecurityPreset(tlsReal bool) SecurityPreset
DefaultSecurityPreset is what the dashboard's "Security headers" button applies.
type Shield ¶
type Shield struct {
Enabled bool `json:"enabled"`
// RatePerMinute is the sustained per-client-IP request rate; Burst is the
// bucket size (default RatePerMinute/6, at least 1).
RatePerMinute int `json:"rate_per_minute,omitempty"`
Burst int `json:"burst,omitempty"`
// MaxConcurrent caps in-flight requests per client IP.
MaxConcurrent int `json:"max_concurrent,omitempty"`
// MaxBodyBytes rejects larger request bodies with 413.
MaxBodyBytes int64 `json:"max_body_bytes,omitempty"`
// BlockBots turns away user agents on the shipped denylist plus BotAgents.
BlockBots bool `json:"block_bots,omitempty"`
BotAgents []string `json:"bot_agents,omitempty"`
// Allow lists addresses and CIDRs the shield never touches.
Allow []string `json:"allow,omitempty"`
// RetryAfterSeconds is sent with 429 (default from the instance limits).
RetryAfterSeconds int `json:"retry_after_seconds,omitempty"`
// TarpitMillis delays a rejection, holding the client's connection; the
// instance caps both the delay and the number of delayed requests.
TarpitMillis int `json:"tarpit_ms,omitempty"`
// UnderAttackUntil tightens every limit until that time, then lapses.
UnderAttackUntil *time.Time `json:"under_attack_until,omitempty"`
}
Shield limits how much one client can cost a domain. Zero values turn an individual protection off.
func (Shield) Effective ¶
func (s Shield) Effective(now time.Time, l Limits) ShieldEffective
Effective resolves the shield at now, applying the under-attack clamp.
func (Shield) UnderAttack ¶
UnderAttack reports whether emergency mode is on at now.
type ShieldEffective ¶
type ShieldEffective struct {
Active bool
UnderAttack bool
RatePerMinute int
Burst int
MaxConcurrent int
MaxBodyBytes int64
BlockBots bool
RetryAfter int
Tarpit time.Duration
}
ShieldEffective is what the handler enforces at one instant.
type ShieldReason ¶
type ShieldReason int
ShieldReason names why the shield turned a request away.
const ( ShieldRateLimit ShieldReason = iota ShieldConcurrency ShieldBadBot ShieldBodyTooLarge ShieldReasonCount )
Shield reasons, in the order their counters are stored.
func (ShieldReason) String ¶
func (r ShieldReason) String() string
String is the reason's stable wire and metric name.
type Step ¶
type Step struct {
Stage string `json:"stage"`
Outcome string `json:"outcome"`
Final bool `json:"final,omitempty"`
}
Step is one stage of the human readable "what happens to this request" preview, in the order the ingress evaluates them.
type ValidationError ¶
type ValidationError struct {
Kind string `json:"kind"`
Fields []FieldError `json:"fields"`
}
ValidationError collects every FieldError found in one section.
func AsValidationError ¶
func AsValidationError(err error) (*ValidationError, bool)
AsValidationError unwraps err into a ValidationError when it is one.
func (*ValidationError) Error ¶
func (e *ValidationError) Error() string