blob: eeb6ef5b8ab0b812761a7f19e1d20d4f3f728d8d [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package simulation
import (
"errors"
"fmt"
"net"
"net/http"
"regexp"
"strings"
"testing"
)
import (
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
tls "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/yl2chen/cidranger"
istiolog "istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pilot/pkg/model"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/networking/core/v1alpha3"
"github.com/apache/dubbo-go-pixiu/pilot/pkg/xds"
xdsfilters "github.com/apache/dubbo-go-pixiu/pilot/pkg/xds/filters"
"github.com/apache/dubbo-go-pixiu/pilot/test/xdstest"
"github.com/apache/dubbo-go-pixiu/pkg/config/host"
"github.com/apache/dubbo-go-pixiu/pkg/test"
"github.com/apache/dubbo-go-pixiu/pkg/util/sets"
)
var log = istiolog.RegisterScope("simulation", "", 0)
type Protocol string
const (
HTTP Protocol = "http"
HTTP2 Protocol = "http2"
TCP Protocol = "tcp"
)
type TLSMode string
const (
Plaintext TLSMode = "plaintext"
TLS TLSMode = "tls"
MTLS TLSMode = "mtls"
)
func (c Call) IsHTTP() bool {
return httpProtocols.Contains(string(c.Protocol)) && (c.TLS == Plaintext || c.TLS == "")
}
var httpProtocols = sets.New(string(HTTP), string(HTTP2))
var (
ErrNoListener = errors.New("no listener matched")
ErrNoFilterChain = errors.New("no filter chains matched")
ErrNoRoute = errors.New("no route matched")
ErrTLSRedirect = errors.New("tls required, sending 301")
ErrNoVirtualHost = errors.New("no virtual host matched")
ErrMultipleFilterChain = errors.New("multiple filter chains matched")
// ErrProtocolError happens when sending TLS/TCP request to HCM, for example
ErrProtocolError = errors.New("protocol error")
ErrTLSError = errors.New("invalid TLS")
ErrMTLSError = errors.New("invalid mTLS")
)
type Expect struct {
Name string
Call Call
Result Result
}
type CallMode string
type CustomFilterChainValidation func(filterChain *listener.FilterChain) error
var (
// CallModeGateway simulate no iptables
CallModeGateway CallMode = "gateway"
// CallModeOutbound simulate iptables redirect to 15001
CallModeOutbound CallMode = "outbound"
// CallModeInbound simulate iptables redirect to 15006
CallModeInbound CallMode = "inbound"
)
type Call struct {
Address string
Port int
Path string
// Protocol describes the protocol type. TLS encapsulation is separate
Protocol Protocol
// TLS describes the connection tls parameters
// TODO: currently this does not verify TLS vs mTLS
TLS TLSMode
Alpn string
// HostHeader is a convenience field for Headers
HostHeader string
Headers http.Header
Sni string
// CallMode describes the type of call to make.
CallMode CallMode
CustomListenerValidations []CustomFilterChainValidation
MtlsSecretConfigName string
}
func (c Call) FillDefaults() Call {
if c.Headers == nil {
c.Headers = http.Header{}
}
if c.HostHeader != "" {
c.Headers["Host"] = []string{c.HostHeader}
}
// For simplicity, set SNI automatically for TLS traffic.
if c.Sni == "" && (c.TLS == TLS) {
c.Sni = c.HostHeader
}
if c.Path == "" {
c.Path = "/"
}
if c.TLS == "" {
c.TLS = Plaintext
}
if c.Address == "" {
// pick a random address, assumption is the test does not care
c.Address = "1.3.3.7"
}
if c.TLS == MTLS && c.Alpn == "" {
c.Alpn = protocolToMTLSAlpn(c.Protocol)
}
if c.TLS == TLS && c.Alpn == "" {
c.Alpn = protocolToTLSAlpn(c.Protocol)
}
return c
}
type Result struct {
Error error
ListenerMatched string
FilterChainMatched string
RouteMatched string
RouteConfigMatched string
VirtualHostMatched string
ClusterMatched string
// StrictMatch controls whether we will strictly match the result. If unset, empty fields will
// be ignored, allowing testing only fields we care about This allows asserting that the result
// is *exactly* equal, allowing asserting a field is empty
StrictMatch bool
// If set, this will mark a test as skipped. Note the result is still checked first - we skip only
// if we pass the test. This is to ensure that if the behavior changes, we still capture it; the skip
// just ensures we notice a test is wrong
Skip string
t test.Failer
}
func (r Result) Matches(t *testing.T, want Result) {
t.Helper()
r.StrictMatch = want.StrictMatch // to make diff pass
r.Skip = want.Skip // to make diff pass
diff := cmp.Diff(want, r, cmpopts.IgnoreUnexported(Result{}), cmpopts.EquateErrors())
if want.StrictMatch && diff != "" {
t.Errorf("Diff: %v", diff)
return
}
if want.Error != r.Error {
t.Errorf("want error %v got %v", want.Error, r.Error)
}
if want.ListenerMatched != "" && want.ListenerMatched != r.ListenerMatched {
t.Errorf("want listener matched %q got %q", want.ListenerMatched, r.ListenerMatched)
} else {
// Populate each field in case we did not care about it. This avoids confusing errors when we have fields
// we don't care about in the test that are present in the result.
want.ListenerMatched = r.ListenerMatched
}
if want.FilterChainMatched != "" && want.FilterChainMatched != r.FilterChainMatched {
t.Errorf("want filter chain matched %q got %q", want.FilterChainMatched, r.FilterChainMatched)
} else {
want.FilterChainMatched = r.FilterChainMatched
}
if want.RouteMatched != "" && want.RouteMatched != r.RouteMatched {
t.Errorf("want route matched %q got %q", want.RouteMatched, r.RouteMatched)
} else {
want.RouteMatched = r.RouteMatched
}
if want.RouteConfigMatched != "" && want.RouteConfigMatched != r.RouteConfigMatched {
t.Errorf("want route config matched %q got %q", want.RouteConfigMatched, r.RouteConfigMatched)
} else {
want.RouteConfigMatched = r.RouteConfigMatched
}
if want.VirtualHostMatched != "" && want.VirtualHostMatched != r.VirtualHostMatched {
t.Errorf("want virtual host matched %q got %q", want.VirtualHostMatched, r.VirtualHostMatched)
} else {
want.VirtualHostMatched = r.VirtualHostMatched
}
if want.ClusterMatched != "" && want.ClusterMatched != r.ClusterMatched {
t.Errorf("want cluster matched %q got %q", want.ClusterMatched, r.ClusterMatched)
} else {
want.ClusterMatched = r.ClusterMatched
}
if t.Failed() {
t.Logf("Diff: %+v", diff)
t.Logf("Full Diff: %+v", cmp.Diff(want, r, cmpopts.IgnoreUnexported(Result{}), cmpopts.EquateErrors()))
} else if want.Skip != "" {
t.Skip(fmt.Sprintf("Known bug: %v", r.Skip))
}
}
type Simulation struct {
t *testing.T
Listeners []*listener.Listener
Clusters []*cluster.Cluster
Routes []*route.RouteConfiguration
}
func NewSimulationFromConfigGen(t *testing.T, s *v1alpha3.ConfigGenTest, proxy *model.Proxy) *Simulation {
l := s.Listeners(proxy)
sim := &Simulation{
t: t,
Listeners: l,
Clusters: s.Clusters(proxy),
Routes: s.RoutesFromListeners(proxy, l),
}
return sim
}
func NewSimulation(t *testing.T, s *xds.FakeDiscoveryServer, proxy *model.Proxy) *Simulation {
return NewSimulationFromConfigGen(t, s.ConfigGenTest, proxy)
}
// withT swaps out the testing struct. This allows executing sub tests.
func (sim *Simulation) withT(t *testing.T) *Simulation {
cpy := *sim
cpy.t = t
return &cpy
}
func (sim *Simulation) RunExpectations(es []Expect) {
for _, e := range es {
sim.t.Run(e.Name, func(t *testing.T) {
sim.withT(t).Run(e.Call).Matches(t, e.Result)
})
}
}
func hasFilterOnPort(l *listener.Listener, filter string, port int) bool {
got, f := xdstest.ExtractListenerFilters(l)[filter]
if !f {
return false
}
if got.FilterDisabled == nil {
return true
}
return !xdstest.EvaluateListenerFilterPredicates(got.FilterDisabled, port)
}
func (sim *Simulation) Run(input Call) (result Result) {
result = Result{t: sim.t}
input = input.FillDefaults()
if input.Alpn != "" && input.TLS == Plaintext {
result.Error = fmt.Errorf("invalid call, ALPN can only be sent in TLS requests")
return result
}
// First we will match a listener
l := matchListener(sim.Listeners, input)
if l == nil {
result.Error = ErrNoListener
return
}
result.ListenerMatched = l.Name
hasTLSInspector := hasFilterOnPort(l, xdsfilters.TLSInspector.Name, input.Port)
if !hasTLSInspector {
// Without tls inspector, Envoy would not read the ALPN in the TLS handshake
// HTTP inspector still may set it though
input.Alpn = ""
}
// Apply listener filters
if hasFilterOnPort(l, xdsfilters.HTTPInspector.Name, input.Port) {
if alpn := protocolToAlpn(input.Protocol); alpn != "" && input.TLS == Plaintext {
input.Alpn = alpn
}
}
fc, err := sim.matchFilterChain(l.FilterChains, l.DefaultFilterChain, input, hasTLSInspector)
if err != nil {
result.Error = err
return
}
result.FilterChainMatched = fc.Name
// Plaintext to TLS is an error
if fc.TransportSocket != nil && input.TLS == Plaintext {
result.Error = ErrTLSError
return
}
mTLSSecretConfigName := "default"
if input.MtlsSecretConfigName != "" {
mTLSSecretConfigName = input.MtlsSecretConfigName
}
// mTLS listener will only accept mTLS traffic
if fc.TransportSocket != nil && sim.requiresMTLS(fc, mTLSSecretConfigName) != (input.TLS == MTLS) {
// If there is no tls inspector, then
result.Error = ErrMTLSError
return
}
if len(input.CustomListenerValidations) > 0 {
for _, validation := range input.CustomListenerValidations {
if err := validation(fc); err != nil {
result.Error = err
}
}
}
if hcm := xdstest.ExtractHTTPConnectionManager(sim.t, fc); hcm != nil {
// We matched HCM and didn't terminate TLS, but we are sending TLS traffic - decoding will fail
if input.TLS != Plaintext && fc.TransportSocket == nil {
result.Error = ErrProtocolError
return
}
// TCP to HCM is invalid
if input.Protocol != HTTP && input.Protocol != HTTP2 {
result.Error = ErrProtocolError
return
}
// Fetch inline route
rc := hcm.GetRouteConfig()
if rc == nil {
// If not set, fallback to RDS
routeName := hcm.GetRds().RouteConfigName
result.RouteConfigMatched = routeName
rc = xdstest.ExtractRouteConfigurations(sim.Routes)[routeName]
}
hostHeader := ""
if len(input.Headers["Host"]) > 0 {
hostHeader = input.Headers["Host"][0]
}
vh := sim.matchVirtualHost(rc, hostHeader)
if vh == nil {
result.Error = ErrNoVirtualHost
return
}
result.VirtualHostMatched = vh.Name
if vh.RequireTls == route.VirtualHost_ALL && input.TLS == Plaintext {
result.Error = ErrTLSRedirect
return
}
r := sim.matchRoute(vh, input)
if r == nil {
result.Error = ErrNoRoute
return
}
result.RouteMatched = r.Name
switch t := r.GetAction().(type) {
case *route.Route_Route:
result.ClusterMatched = t.Route.GetCluster()
}
} else if tcp := xdstest.ExtractTCPProxy(sim.t, fc); tcp != nil {
result.ClusterMatched = tcp.GetCluster()
}
return
}
func (sim *Simulation) requiresMTLS(fc *listener.FilterChain, mTLSSecretConfigName string) bool {
if fc.TransportSocket == nil {
return false
}
t := &tls.DownstreamTlsContext{}
if err := fc.GetTransportSocket().GetTypedConfig().UnmarshalTo(t); err != nil {
sim.t.Fatal(err)
}
if len(t.GetCommonTlsContext().GetTlsCertificateSdsSecretConfigs()) == 0 {
return false
}
// This is a lazy heuristic, we could check for explicit default resource or spiffe if it becomes necessary
if t.GetCommonTlsContext().GetTlsCertificateSdsSecretConfigs()[0].Name != mTLSSecretConfigName {
return false
}
if !t.RequireClientCertificate.Value {
return false
}
return true
}
func (sim *Simulation) matchRoute(vh *route.VirtualHost, input Call) *route.Route {
for _, r := range vh.Routes {
// check path
switch pt := r.Match.GetPathSpecifier().(type) {
case *route.RouteMatch_Prefix:
if !strings.HasPrefix(input.Path, pt.Prefix) {
continue
}
case *route.RouteMatch_Path:
if input.Path != pt.Path {
continue
}
case *route.RouteMatch_SafeRegex:
r, err := regexp.Compile(pt.SafeRegex.GetRegex())
if err != nil {
sim.t.Fatalf("invalid regex %v: %v", pt.SafeRegex.GetRegex(), err)
}
if !r.MatchString(input.Path) {
continue
}
default:
sim.t.Fatalf("unknown route path type")
}
// TODO this only handles path - we need to add headers, query params, etc to be complete.
return r
}
return nil
}
func (sim *Simulation) matchVirtualHost(rc *route.RouteConfiguration, host string) *route.VirtualHost {
// Exact match
for _, vh := range rc.VirtualHosts {
for _, d := range vh.Domains {
if d == host {
return vh
}
}
}
// prefix match
var bestMatch *route.VirtualHost
longest := 0
for _, vh := range rc.VirtualHosts {
for _, d := range vh.Domains {
if d[0] != '*' {
continue
}
if len(host) >= len(d) && strings.HasSuffix(host, d[1:]) && len(d) > longest {
bestMatch = vh
longest = len(d)
}
}
}
if bestMatch != nil {
return bestMatch
}
// Suffix match
longest = 0
for _, vh := range rc.VirtualHosts {
for _, d := range vh.Domains {
if d[len(d)-1] != '*' {
continue
}
if len(host) >= len(d) && strings.HasPrefix(host, d[:len(d)-1]) && len(d) > longest {
bestMatch = vh
longest = len(d)
}
}
}
if bestMatch != nil {
return bestMatch
}
// wildcard match
for _, vh := range rc.VirtualHosts {
for _, d := range vh.Domains {
if d == "*" {
return vh
}
}
}
return nil
}
// Follow the 8 step Sieve as in
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener_components.proto.html#config-listener-v3-filterchainmatch
// The implementation may initially be confusing because of a property of the
// Envoy algorithm - at each level we will filter out all FilterChains that do
// not match. This means an empty match (`{}`) may not match if another chain
// matches one criteria but not another.
func (sim *Simulation) matchFilterChain(chains []*listener.FilterChain, defaultChain *listener.FilterChain,
input Call, hasTLSInspector bool) (*listener.FilterChain, error) {
chains = filter("DestinationPort", chains, func(fc *listener.FilterChainMatch) bool {
return fc.GetDestinationPort() == nil
}, func(fc *listener.FilterChainMatch) bool {
return int(fc.GetDestinationPort().GetValue()) == input.Port
})
chains = filter("PrefixRanges", chains, func(fc *listener.FilterChainMatch) bool {
return fc.GetPrefixRanges() == nil
}, func(fc *listener.FilterChainMatch) bool {
ranger := cidranger.NewPCTrieRanger()
for _, a := range fc.GetPrefixRanges() {
s := fmt.Sprintf("%s/%d", a.AddressPrefix, a.GetPrefixLen().GetValue())
_, cidr, err := net.ParseCIDR(s)
if err != nil {
sim.t.Fatalf("failed to parse cidr %v: %v", s, err)
}
if err := ranger.Insert(cidranger.NewBasicRangerEntry(*cidr)); err != nil {
sim.t.Fatalf("failed to insert cidr %v: %v", cidr, err)
}
}
f, err := ranger.Contains(net.ParseIP(input.Address))
if err != nil {
sim.t.Fatalf("cidr containers %v failed: %v", input.Address, err)
}
return f
})
chains = filter("ServerNames", chains, func(fc *listener.FilterChainMatch) bool {
return fc.GetServerNames() == nil
}, func(fc *listener.FilterChainMatch) bool {
sni := host.Name(input.Sni)
for _, s := range fc.GetServerNames() {
if sni.SubsetOf(host.Name(s)) {
return true
}
}
return false
})
chains = filter("TransportProtocol", chains, func(fc *listener.FilterChainMatch) bool {
return fc.GetTransportProtocol() == ""
}, func(fc *listener.FilterChainMatch) bool {
if !hasTLSInspector {
// Without tls inspector, transport protocol will always be raw buffer
return fc.GetTransportProtocol() == xdsfilters.RawBufferTransportProtocol
}
switch fc.GetTransportProtocol() {
case xdsfilters.TLSTransportProtocol:
return input.TLS == TLS || input.TLS == MTLS
case xdsfilters.RawBufferTransportProtocol:
return input.TLS == Plaintext
}
return false
})
chains = filter("ApplicationProtocols", chains, func(fc *listener.FilterChainMatch) bool {
return fc.GetApplicationProtocols() == nil
}, func(fc *listener.FilterChainMatch) bool {
return sets.New(fc.GetApplicationProtocols()...).Contains(input.Alpn)
})
// We do not implement the "source" based filters as we do not use them
if len(chains) > 1 {
for _, c := range chains {
log.Warnf("Matched chain %v", c.Name)
}
return nil, ErrMultipleFilterChain
}
if len(chains) == 0 {
if defaultChain != nil {
return defaultChain, nil
}
return nil, ErrNoFilterChain
}
return chains[0], nil
}
func filter(desc string, chains []*listener.FilterChain,
empty func(fc *listener.FilterChainMatch) bool,
match func(fc *listener.FilterChainMatch) bool) []*listener.FilterChain {
res := []*listener.FilterChain{}
anySet := false
for _, c := range chains {
if !empty(c.GetFilterChainMatch()) {
anySet = true
break
}
}
if !anySet {
log.Debugf("%v: none set, skipping", desc)
return chains
}
for i, c := range chains {
if match(c.GetFilterChainMatch()) {
log.Debugf("%v: matched chain %v/%v", desc, i, c.GetName())
res = append(res, c)
}
}
// Return all matching filter chains
if len(res) > 0 {
return res
}
// Unless there were no matches - in which case we return all filter chains that did not have a
// match set
for i, c := range chains {
if empty(c.GetFilterChainMatch()) {
log.Debugf("%v: no matches, found empty chain match %v/%v", desc, i, c.GetName())
res = append(res, c)
}
}
return res
}
func protocolToMTLSAlpn(s Protocol) string {
switch s {
case HTTP:
return "istio-http/1.1"
case HTTP2:
return "istio-h2"
default:
return "istio"
}
}
func protocolToTLSAlpn(s Protocol) string {
switch s {
case HTTP:
return "http/1.1"
case HTTP2:
return "h2"
default:
return ""
}
}
func protocolToAlpn(s Protocol) string {
switch s {
case HTTP:
return "http/1.1"
case HTTP2:
return "h2c"
default:
return ""
}
}
func matchListener(listeners []*listener.Listener, input Call) *listener.Listener {
if input.CallMode == CallModeInbound {
return xdstest.ExtractListener(model.VirtualInboundListenerName, listeners)
}
// First find exact match for the IP/Port, then fallback to wildcard IP/Port
// There is no wildcard port
for _, l := range listeners {
if matchAddress(l.GetAddress(), input.Address, input.Port) {
return l
}
}
for _, l := range listeners {
if matchAddress(l.GetAddress(), "0.0.0.0", input.Port) {
return l
}
}
// Fallback to the outbound listener
// TODO - support inbound
for _, l := range listeners {
if l.Name == model.VirtualOutboundListenerName {
return l
}
}
return nil
}
func matchAddress(a *core.Address, address string, port int) bool {
if a.GetSocketAddress().GetAddress() != address {
return false
}
if int(a.GetSocketAddress().GetPortValue()) != port {
return false
}
return true
}