diff --git a/pkg/config/protocol/instance.go b/pkg/config/protocol/instance.go index 276f0864b8c0..d8973f9ff993 100644 --- a/pkg/config/protocol/instance.go +++ b/pkg/config/protocol/instance.go @@ -54,6 +54,9 @@ const ( Redis Instance = "Redis" // MySQL declares that the port carries MySQL traffic. MySQL Instance = "MySQL" + // HBONE declares that the port carries HBONE traffic. + // This cannot be declared by Services, but is used for some internal code that uses Protocol + HBONE Instance = "HBONE" // Unsupported - value to signify that the protocol is unsupported. Unsupported Instance = "UnsupportedProtocol" ) diff --git a/pkg/config/protocol/instance_test.go b/pkg/config/protocol/instance_test.go index c63b10edca75..1a26e7d4e03f 100644 --- a/pkg/config/protocol/instance_test.go +++ b/pkg/config/protocol/instance_test.go @@ -60,6 +60,7 @@ func TestParse(t *testing.T) { {"MySQL", protocol.MySQL}, {"", protocol.Unsupported}, {"SMTP", protocol.Unsupported}, + {"HBONE", protocol.Unsupported}, } for _, testPair := range testPairs { diff --git a/pkg/hbone/README.md b/pkg/hbone/README.md new file mode 100644 index 000000000000..94a501fce433 --- /dev/null +++ b/pkg/hbone/README.md @@ -0,0 +1,71 @@ +# HTTP Based Overlay Network (HBONE) + +HTTP Based Overlay Network (HBONE) is the protocol used by Istio for communication between workloads in the mesh. +At a high level, the protocol consists of tunneling TCP connections over HTTP/2 CONNECT, over mTLS. + +## Specification + +TODO + +## Implementations + +### Clients + +#### CLI + +A CLI client is available using the `client` binary. + +Usage examples: + +```shell +go install ./pkg/test/echo/cmd/client +# Send request to 127.0.0.1:8080 (Note only IPs are supported) via an HBONE proxy on port 15008 +client --hbone-client-cert tests/testdata/certs/cert.crt --hbone-client-key tests/testdata/certs/cert.key \ + http://127.0.0.1:8080 \ + --hbone 127.0.0.1:15008 +``` + +#### Golang + +An (unstable) library to make HBONE connections is available at `pkg/hbone`. + +Usage example: + +```go +d := hbone.NewDialer(hbone.Config{ + ProxyAddress: "1.2.3.4:15008", + Headers: map[string][]string{ + "some-addition-metadata": {"test-value"}, + }, + TLS: nil, // TLS is strongly recommended in real world +}) +client, _ := d.Dial("tcp", testAddr) +client.Write([]byte("hello world")) +``` + +### Server + +#### Server CLI + +A CLI client is available using the `server` binary. + +Usage examples: + +```shell +go install ./pkg/test/echo/cmd/server +# Serve on port 15008 (default) with TLS +server --tls 15008 --crt tests/testdata/certs/cert.crt --key tests/testdata/certs/cert.key +``` + +#### Server Golang Library + +An (unstable) library to run an HBONE server is available at `pkg/hbone`. + +Usage example: + +```go +s := hbone.NewServer() +// TLS is strongly recommended in real world +l, _ := net.Listen("tcp", "0.0.0.0:15008") +s.Serve(l) +``` diff --git a/pkg/hbone/dialer.go b/pkg/hbone/dialer.go new file mode 100644 index 000000000000..7c1470d08192 --- /dev/null +++ b/pkg/hbone/dialer.go @@ -0,0 +1,184 @@ +// 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 hbone + +import ( + "context" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "golang.org/x/net/http2" + "golang.org/x/net/proxy" + + "istio.io/istio/security/pkg/pki/util" + istiolog "istio.io/pkg/log" +) + +var log = istiolog.RegisterScope("hbone", "", 0) + +// Config defines the configuration for a given dialer. All fields other than ProxyAddress are optional +type Config struct { + // ProxyAddress defines the address of the HBONE proxy we are connecting to + ProxyAddress string + Headers http.Header + TLS *tls.Config +} + +type Dialer interface { + proxy.Dialer + proxy.ContextDialer +} + +// NewDialer creates a Dialer that proxies connections over HBONE to the configured proxy. +func NewDialer(cfg Config) Dialer { + var transport *http2.Transport + if cfg.TLS != nil { + transport = &http2.Transport{ + TLSClientConfig: cfg.TLS, + } + } else { + transport = &http2.Transport{ + // For h2c + AllowHTTP: true, + DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) { + return net.Dial(network, addr) + }, + } + } + return &dialer{ + cfg: cfg, + transport: transport, + } +} + +type dialer struct { + cfg Config + transport *http2.Transport +} + +// DialContext connects to `address` via the HBONE proxy. +func (d *dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if network != "tcp" { + return net.Dial(network, address) + } + // TODO: use context + c, s := net.Pipe() + err := d.proxyTo(s, d.cfg, address) + if err != nil { + return nil, err + } + return c, nil +} + +func (d dialer) Dial(network, address string) (c net.Conn, err error) { + return d.DialContext(context.Background(), network, address) +} + +func (d *dialer) proxyTo(conn io.ReadWriteCloser, req Config, address string) error { + t0 := time.Now() + + url := "http://" + req.ProxyAddress + if req.TLS != nil { + url = "https://" + req.ProxyAddress + } + // Setup a pipe. We could just pass `conn` to `http.NewRequest`, but this has a few issues: + // * Less visibility into i/o + // * http will call conn.Close, which will close before we want to (finished writing response). + pr, pw := io.Pipe() + r, err := http.NewRequest(http.MethodConnect, url, pr) + if err != nil { + return fmt.Errorf("new request: %v", err) + } + r.Host = address + + // Initiate CONNECT. + log.Infof("initiate CONNECT to %v via %v", r.Host, url) + + resp, err := d.transport.RoundTrip(r) + if err != nil { + return fmt.Errorf("round trip: %v", err) + } + var remoteID string + if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 { + ids, _ := util.ExtractIDs(resp.TLS.PeerCertificates[0].Extensions) + if len(ids) > 0 { + remoteID = ids[0] + } + } + log.WithLabels("host", r.Host, "remote", remoteID).Info("CONNECT established") + go func() { + defer conn.Close() + defer resp.Body.Close() + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + // handle upstream (hbone server) --> downstream (app) + copyBuffered(conn, resp.Body, log.WithLabels("name", "body to conn")) + wg.Done() + }() + // Copy from conn into the pipe, which will then be sent as part of the request + // handle upstream (hbone server) <-- downstream (app) + copyBuffered(pw, conn, log.WithLabels("name", "conn to pipe")) + + wg.Wait() + log.Info("stream closed in ", time.Since(t0)) + }() + + return nil +} + +// TLSDialWithDialer is an implementation of tls.DialWithDialer that accepts a generic Dialer +func TLSDialWithDialer(dialer Dialer, network, addr string, config *tls.Config) (*tls.Conn, error) { + return tlsDial(context.Background(), dialer, network, addr, config) +} + +func tlsDial(ctx context.Context, netDialer Dialer, network, addr string, config *tls.Config) (*tls.Conn, error) { + rawConn, err := netDialer.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + + colonPos := strings.LastIndex(addr, ":") + if colonPos == -1 { + colonPos = len(addr) + } + hostname := addr[:colonPos] + + if config == nil { + config = &tls.Config{} + } + // If no ServerName is set, infer the ServerName + // from the hostname we're connecting to. + if config.ServerName == "" { + // Make a copy to avoid polluting argument or default. + c := config.Clone() + c.ServerName = hostname + config = c + } + + conn := tls.Client(rawConn, config) + if err := conn.HandshakeContext(ctx); err != nil { + rawConn.Close() + return nil, err + } + return conn, nil +} diff --git a/pkg/hbone/dialer_test.go b/pkg/hbone/dialer_test.go new file mode 100644 index 000000000000..615196b8a7c7 --- /dev/null +++ b/pkg/hbone/dialer_test.go @@ -0,0 +1,110 @@ +// 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 hbone + +import ( + "net" + "testing" +) + +func newTCPServer(t testing.TB, data string) string { + n, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + t.Logf("opened listener on %v", n.Addr().String()) + go func() { + for { + c, err := n.Accept() + if err != nil { + t.Log(err) + return + } + t.Log("accepted connection") + c.Write([]byte(data)) + c.Close() + } + }() + t.Cleanup(func() { + n.Close() + }) + return n.Addr().String() +} + +func TestDialerError(t *testing.T) { + d := NewDialer(Config{ + ProxyAddress: "127.0.0.10:1", // Random address that should fail to dial + Headers: map[string][]string{ + "some-addition-metadata": {"test-value"}, + }, + TLS: nil, // No TLS for simplification + }) + _, err := d.Dial("tcp", "fake") + if err == nil { + t.Fatal("expected error, got none.") + } +} + +func TestDialer(t *testing.T) { + testAddr := newTCPServer(t, "hello") + proxy := newHBONEServer(t) + d := NewDialer(Config{ + ProxyAddress: proxy, + Headers: map[string][]string{ + "some-addition-metadata": {"test-value"}, + }, + TLS: nil, // No TLS for simplification + }) + send := func() { + client, err := d.Dial("tcp", testAddr) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + go func() { + n, err := client.Write([]byte("hello world")) + t.Logf("wrote %v/%v", n, err) + }() + + buf := make([]byte, 8) + n, err := client.Read(buf) + if err != nil { + t.Fatalf("err with %v: %v", n, err) + } + if string(buf[:n]) != "hello" { + t.Fatalf("got unexpected buffer: %v", string(buf[:n])) + } + t.Logf("Read %v", string(buf[:n])) + } + // Make sure we can create multiple connections + send() + send() +} + +func newHBONEServer(t *testing.T) string { + s := NewServer() + l, err := net.Listen("tcp", "0.0.0.0:0") + if err != nil { + t.Fatal(err) + } + go func() { + _ = s.Serve(l) + }() + t.Cleanup(func() { + _ = l.Close() + }) + return l.Addr().String() +} diff --git a/pkg/hbone/server.go b/pkg/hbone/server.go new file mode 100644 index 000000000000..8155d079d951 --- /dev/null +++ b/pkg/hbone/server.go @@ -0,0 +1,83 @@ +// 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 hbone + +import ( + "context" + "net" + "net/http" + "sync" + "time" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +func NewServer() *http.Server { + // Need to set this to allow timeout on the read header + h1 := &http.Transport{ + ExpectContinueTimeout: 3 * time.Second, + } + h2, _ := http2.ConfigureTransports(h1) + h2.ReadIdleTimeout = 10 * time.Minute // TODO: much larger to support long-lived connections + h2.AllowHTTP = true + h2Server := &http2.Server{} + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + if handleConnect(w, r) { + return + } + } else { + log.Errorf("non-CONNECT: %v", r.Method) + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + hs := &http.Server{ + Handler: h2c.NewHandler(handler, h2Server), + } + return hs +} + +func handleConnect(w http.ResponseWriter, r *http.Request) bool { + t0 := time.Now() + log.WithLabels("host", r.Host, "source", r.RemoteAddr).Info("Received CONNECT") + // Send headers back immediately so we can start getting the body + w.(http.Flusher).Flush() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + dst, err := (&net.Dialer{}).DialContext(ctx, "tcp", r.Host) + if err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + log.Errorf("failed to dial upstream: %v", err) + return true + } + log.Infof("Connected to %v", r.Host) + w.WriteHeader(http.StatusOK) + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + // downstream (hbone client) <-- upstream (app) + copyBuffered(w, dst, log.WithLabels("name", "dst to w")) + r.Body.Close() + wg.Done() + }() + // downstream (hbone client) --> upstream (app) + copyBuffered(dst, r.Body, log.WithLabels("name", "body to dst")) + wg.Wait() + log.Infof("connection closed in %v", time.Since(t0)) + return false +} diff --git a/pkg/hbone/util.go b/pkg/hbone/util.go new file mode 100644 index 000000000000..f321ee0590b5 --- /dev/null +++ b/pkg/hbone/util.go @@ -0,0 +1,112 @@ +// 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 hbone + +import ( + "io" + "net" + "net/http" + "sync" + "time" + + istiolog "istio.io/pkg/log" +) + +// createBuffer to get a buffer. io.Copy uses 32k. +// experimental use shows ~20k max read with Firefox. +var bufferPoolCopy = sync.Pool{New: func() interface{} { + return make([]byte, 0, 32*1024) +}} + +func copyBuffered(dst io.Writer, src io.Reader, log *istiolog.Scope) { + buf1 := bufferPoolCopy.Get().([]byte) + // nolint: staticcheck + defer bufferPoolCopy.Put(buf1) + bufCap := cap(buf1) + buf := buf1[0:bufCap:bufCap] + + // For netstack: src is a gonet.Conn, doesn't implement WriterTo. Dst is a net.TcpConn - and implements ReadFrom. + // CopyBuffered is the actual implementation of Copy and CopyBuffer. + // if buf is nil, one is allocated. + // Duplicated from io + + // This will prevent stats from working. + // If the reader has a WriteTo method, use it to do the copy. + // Avoids an allocation and a copy. + //if wt, ok := src.(io.WriterTo); ok { + // return wt.WriteTo(dst) + //} + // Similarly, if the writer has a ReadFrom method, use it to do the copy. + //if rt, ok := dst.(io.ReaderFrom); ok { + // return rt.ReadFrom(src) + //} + for { + if srcc, ok := src.(net.Conn); ok { + // Best effort + _ = srcc.SetReadDeadline(time.Now().Add(15 * time.Minute)) + } + nr, err := src.Read(buf) + log.Debugf("read %v/%v", nr, err) + if nr > 0 { // before dealing with the read error + nw, ew := dst.Write(buf[0:nr]) + log.Debugf("write %v/%v", nw, ew) + if f, ok := dst.(http.Flusher); ok { + f.Flush() + } + if nr != nw { // Should not happen + ew = io.ErrShortWrite + } + if ew != nil { + return + } + } + if err != nil { + // read is already closed - we need to close out + _ = closeWriter(dst) + return + } + } +} + +// CloseWriter is one of possible interfaces implemented by Out to send a FIN, without closing +// the input. Some writers only do this when Close is called. +type CloseWriter interface { + CloseWrite() error +} + +func closeWriter(dst io.Writer) error { + if cw, ok := dst.(CloseWriter); ok { + return cw.CloseWrite() + } + if c, ok := dst.(io.Closer); ok { + return c.Close() + } + if rw, ok := dst.(http.ResponseWriter); ok { + // Server side HTTP stream. For client side, FIN can be sent by closing the pipe (or + // request body). For server, the FIN will be sent when the handler returns - but + // this only happen after request is completed and body has been read. If server wants + // to send FIN first - while still reading the body - we are in trouble. + + // That means HTTP2 TCP servers provide no way to send a FIN from server, without + // having the request fully read. + + // This works for H2 with the current library - but very tricky, if not set as trailer. + rw.Header().Set("X-Close", "0") + rw.(http.Flusher).Flush() + return nil + } + log.Infof("Server out not Closer nor CloseWriter nor ResponseWriter: %v", dst) + return nil +} diff --git a/pkg/test/echo/cmd/client/main.go b/pkg/test/echo/cmd/client/main.go index 29cf0d8d6c1e..1a260144ee61 100644 --- a/pkg/test/echo/cmd/client/main.go +++ b/pkg/test/echo/cmd/client/main.go @@ -56,11 +56,18 @@ var ( followRedirects bool newConnectionPerRequest bool forceDNSLookup bool - clientCert string - clientKey string + + clientCert string + clientKey string caFile string + hboneAddress string + hboneHeaders []string + hboneClientCert string + hboneClientKey string + hboneCaFile string + loggingOptions = log.DefaultOptions() rootCmd = &cobra.Command{ @@ -151,6 +158,13 @@ func init() { rootCmd.PersistentFlags().StringSliceVarP(&alpn, "alpn", "", nil, "alpn to set") rootCmd.PersistentFlags().StringVarP(&serverName, "server-name", "", serverName, "server name to set") + rootCmd.PersistentFlags().StringVar(&hboneAddress, "hbone", "", "address to send HBONE request to") + rootCmd.PersistentFlags().StringSliceVarP(&hboneHeaders, "hbone-header", "M", hboneHeaders, + "A list of http headers for HBONE connection (use Host for authority) - 'name: value', following curl syntax") + rootCmd.PersistentFlags().StringVar(&hboneCaFile, "hbone-ca", "", "CA root cert file used for the HBONE request") + rootCmd.PersistentFlags().StringVar(&hboneClientCert, "hbone-client-cert", "", "client certificate file used for the HBONE request") + rootCmd.PersistentFlags().StringVar(&hboneClientKey, "hbone-client-key", "", "client certificate key file used for the HBONE request") + loggingOptions.AttachCobraFlags(rootCmd) cmd.AddFlags(rootCmd) @@ -185,6 +199,27 @@ func getRequest(url string) (*proto.ForwardEchoRequest, error) { NewConnectionPerRequest: newConnectionPerRequest, ForceDNSLookup: forceDNSLookup, } + if len(hboneAddress) > 0 { + request.Hbone = &proto.HBONE{ + Address: hboneAddress, + CertFile: hboneClientCert, + KeyFile: hboneClientKey, + CaCertFile: hboneCaFile, + InsecureSkipVerify: false, + } + for _, header := range hboneHeaders { + parts := strings.SplitN(header, ":", 2) + // require name:value format + if len(parts) != 2 { + return nil, fmt.Errorf("invalid header format: %q (want name:value)", header) + } + + request.Hbone.Headers = append(request.Hbone.Headers, &proto.Header{ + Key: parts[0], + Value: strings.Trim(parts[1], " "), + }) + } + } if expectSet { request.ExpectedResponse = &wrappers.StringValue{Value: expect} diff --git a/pkg/test/echo/cmd/server/main.go b/pkg/test/echo/cmd/server/main.go index 30bc9d7a017e..2a948b18faab 100644 --- a/pkg/test/echo/cmd/server/main.go +++ b/pkg/test/echo/cmd/server/main.go @@ -37,6 +37,7 @@ var ( grpcPorts []int tcpPorts []int tlsPorts []int + hbonePorts []int instanceIPPorts []int localhostIPPorts []int serverFirstPorts []int @@ -59,7 +60,7 @@ var ( Long: `Echo application for testing Istio E2E`, PersistentPreRunE: configureLogging, Run: func(cmd *cobra.Command, args []string) { - ports := make(common.PortList, len(httpPorts)+len(grpcPorts)+len(tcpPorts)) + ports := make(common.PortList, len(httpPorts)+len(grpcPorts)+len(tcpPorts)+len(hbonePorts)) tlsByPort := map[int]bool{} for _, p := range tlsPorts { tlsByPort[p] = true @@ -104,6 +105,15 @@ var ( } portIndex++ } + for i, p := range hbonePorts { + ports[portIndex] = &common.Port{ + Name: "hbone-" + strconv.Itoa(i), + Protocol: protocol.HBONE, + Port: p, + TLS: tlsByPort[p], + } + portIndex++ + } instanceIPByPort := map[int]struct{}{} for _, p := range instanceIPPorts { instanceIPByPort[p] = struct{}{} @@ -154,6 +164,7 @@ func init() { rootCmd.PersistentFlags().IntSliceVar(&httpPorts, "port", []int{8080}, "HTTP/1.1 ports") rootCmd.PersistentFlags().IntSliceVar(&grpcPorts, "grpc", []int{7070}, "GRPC ports") rootCmd.PersistentFlags().IntSliceVar(&tcpPorts, "tcp", []int{9090}, "TCP ports") + rootCmd.PersistentFlags().IntSliceVar(&hbonePorts, "hbone", []int{}, "HBONE ports") rootCmd.PersistentFlags().IntSliceVar(&tlsPorts, "tls", []int{}, "Ports that are using TLS. These must be defined as http/grpc/tcp.") rootCmd.PersistentFlags().IntSliceVar(&instanceIPPorts, "bind-ip", []int{}, "Ports that are bound to INSTANCE_IP rather than wildcard IP.") rootCmd.PersistentFlags().IntSliceVar(&localhostIPPorts, "bind-localhost", []int{}, "Ports that are bound to localhost rather than wildcard IP.") diff --git a/pkg/test/echo/proto/echo.pb.go b/pkg/test/echo/proto/echo.pb.go index ff2935fb0638..3c05745b144f 100644 --- a/pkg/test/echo/proto/echo.pb.go +++ b/pkg/test/echo/proto/echo.pb.go @@ -232,6 +232,8 @@ type ForwardEchoRequest struct { NewConnectionPerRequest bool `protobuf:"varint,22,opt,name=newConnectionPerRequest,proto3" json:"newConnectionPerRequest,omitempty"` // If set, each request will force a DNS lookup. Only applies if newConnectionPerRequest is set. ForceDNSLookup bool `protobuf:"varint,23,opt,name=forceDNSLookup,proto3" json:"forceDNSLookup,omitempty"` + // HBONE communication settings. If provided, requests will be tunnelled. + Hbone *HBONE `protobuf:"bytes,24,opt,name=hbone,proto3" json:"hbone,omitempty"` } func (x *ForwardEchoRequest) Reset() { @@ -427,6 +429,129 @@ func (x *ForwardEchoRequest) GetForceDNSLookup() bool { return false } +func (x *ForwardEchoRequest) GetHbone() *HBONE { + if x != nil { + return x.Hbone + } + return nil +} + +type HBONE struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Address string `protobuf:"bytes,9,opt,name=address,proto3" json:"address,omitempty"` + Headers []*Header `protobuf:"bytes,1,rep,name=headers,proto3" json:"headers,omitempty"` + // If non-empty, make the request with the corresponding cert and key. + Cert string `protobuf:"bytes,2,opt,name=cert,proto3" json:"cert,omitempty"` + Key string `protobuf:"bytes,3,opt,name=key,proto3" json:"key,omitempty"` + // If non-empty, verify the server CA + CaCert string `protobuf:"bytes,4,opt,name=caCert,proto3" json:"caCert,omitempty"` + // If non-empty, make the request with the corresponding cert and key file. + CertFile string `protobuf:"bytes,5,opt,name=certFile,proto3" json:"certFile,omitempty"` + KeyFile string `protobuf:"bytes,6,opt,name=keyFile,proto3" json:"keyFile,omitempty"` + // If non-empty, verify the server CA with the ca cert file. + CaCertFile string `protobuf:"bytes,7,opt,name=caCertFile,proto3" json:"caCertFile,omitempty"` + // Skip verifying peer's certificate. + InsecureSkipVerify bool `protobuf:"varint,8,opt,name=insecureSkipVerify,proto3" json:"insecureSkipVerify,omitempty"` +} + +func (x *HBONE) Reset() { + *x = HBONE{} + if protoimpl.UnsafeEnabled { + mi := &file_test_echo_proto_echo_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *HBONE) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HBONE) ProtoMessage() {} + +func (x *HBONE) ProtoReflect() protoreflect.Message { + mi := &file_test_echo_proto_echo_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HBONE.ProtoReflect.Descriptor instead. +func (*HBONE) Descriptor() ([]byte, []int) { + return file_test_echo_proto_echo_proto_rawDescGZIP(), []int{4} +} + +func (x *HBONE) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +func (x *HBONE) GetHeaders() []*Header { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HBONE) GetCert() string { + if x != nil { + return x.Cert + } + return "" +} + +func (x *HBONE) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *HBONE) GetCaCert() string { + if x != nil { + return x.CaCert + } + return "" +} + +func (x *HBONE) GetCertFile() string { + if x != nil { + return x.CertFile + } + return "" +} + +func (x *HBONE) GetKeyFile() string { + if x != nil { + return x.KeyFile + } + return "" +} + +func (x *HBONE) GetCaCertFile() string { + if x != nil { + return x.CaCertFile + } + return "" +} + +func (x *HBONE) GetInsecureSkipVerify() bool { + if x != nil { + return x.InsecureSkipVerify + } + return false +} + type Alpn struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -438,7 +563,7 @@ type Alpn struct { func (x *Alpn) Reset() { *x = Alpn{} if protoimpl.UnsafeEnabled { - mi := &file_test_echo_proto_echo_proto_msgTypes[4] + mi := &file_test_echo_proto_echo_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -451,7 +576,7 @@ func (x *Alpn) String() string { func (*Alpn) ProtoMessage() {} func (x *Alpn) ProtoReflect() protoreflect.Message { - mi := &file_test_echo_proto_echo_proto_msgTypes[4] + mi := &file_test_echo_proto_echo_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -464,7 +589,7 @@ func (x *Alpn) ProtoReflect() protoreflect.Message { // Deprecated: Use Alpn.ProtoReflect.Descriptor instead. func (*Alpn) Descriptor() ([]byte, []int) { - return file_test_echo_proto_echo_proto_rawDescGZIP(), []int{4} + return file_test_echo_proto_echo_proto_rawDescGZIP(), []int{5} } func (x *Alpn) GetValue() []string { @@ -485,7 +610,7 @@ type ForwardEchoResponse struct { func (x *ForwardEchoResponse) Reset() { *x = ForwardEchoResponse{} if protoimpl.UnsafeEnabled { - mi := &file_test_echo_proto_echo_proto_msgTypes[5] + mi := &file_test_echo_proto_echo_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -498,7 +623,7 @@ func (x *ForwardEchoResponse) String() string { func (*ForwardEchoResponse) ProtoMessage() {} func (x *ForwardEchoResponse) ProtoReflect() protoreflect.Message { - mi := &file_test_echo_proto_echo_proto_msgTypes[5] + mi := &file_test_echo_proto_echo_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -511,7 +636,7 @@ func (x *ForwardEchoResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ForwardEchoResponse.ProtoReflect.Descriptor instead. func (*ForwardEchoResponse) Descriptor() ([]byte, []int) { - return file_test_echo_proto_echo_proto_rawDescGZIP(), []int{5} + return file_test_echo_proto_echo_proto_rawDescGZIP(), []int{6} } func (x *ForwardEchoResponse) GetOutput() []string { @@ -536,7 +661,7 @@ var file_test_echo_proto_echo_proto_rawDesc = []byte{ 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x30, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xf9, 0x05, 0x0a, 0x12, 0x46, 0x6f, 0x72, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9d, 0x06, 0x0a, 0x12, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x71, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, @@ -584,23 +709,42 @@ var file_test_echo_proto_echo_proto_rawDesc = []byte{ 0x6f, 0x6e, 0x50, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x44, 0x4e, 0x53, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x18, 0x17, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x44, 0x4e, 0x53, 0x4c, 0x6f, - 0x6f, 0x6b, 0x75, 0x70, 0x22, 0x1c, 0x0a, 0x04, 0x41, 0x6c, 0x70, 0x6e, 0x12, 0x14, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x22, 0x2d, 0x0a, 0x13, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x45, 0x63, 0x68, - 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, - 0x74, 0x32, 0x88, 0x01, 0x0a, 0x0f, 0x45, 0x63, 0x68, 0x6f, 0x54, 0x65, 0x73, 0x74, 0x53, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x12, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6f, - 0x72, 0x77, 0x61, 0x72, 0x64, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, - 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x1f, 0x0a, 0x0d, - 0x69, 0x6f, 0x2e, 0x69, 0x73, 0x74, 0x69, 0x6f, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x42, 0x04, 0x45, - 0x63, 0x68, 0x6f, 0x5a, 0x08, 0x2e, 0x2e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6f, 0x6b, 0x75, 0x70, 0x12, 0x22, 0x0a, 0x05, 0x68, 0x62, 0x6f, 0x6e, 0x65, 0x18, 0x18, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x42, 0x4f, 0x4e, + 0x45, 0x52, 0x05, 0x68, 0x62, 0x6f, 0x6e, 0x65, 0x22, 0x8e, 0x02, 0x0a, 0x05, 0x48, 0x42, 0x4f, + 0x4e, 0x45, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x27, 0x0a, 0x07, + 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52, 0x07, 0x68, 0x65, + 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x65, 0x72, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x63, + 0x61, 0x43, 0x65, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x61, 0x43, + 0x65, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x65, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x65, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x6b, 0x65, 0x79, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x61, 0x43, + 0x65, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, + 0x61, 0x43, 0x65, 0x72, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x69, 0x6e, 0x73, + 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x69, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x53, + 0x6b, 0x69, 0x70, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x22, 0x1c, 0x0a, 0x04, 0x41, 0x6c, 0x70, + 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x2d, 0x0a, 0x13, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x32, 0x88, 0x01, 0x0a, 0x0f, 0x45, 0x63, 0x68, 0x6f, 0x54, + 0x65, 0x73, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x45, 0x63, + 0x68, 0x6f, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x45, + 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x0b, 0x46, + 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x6f, + 0x72, 0x77, 0x61, 0x72, 0x64, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x1f, 0x0a, 0x0d, 0x69, 0x6f, 0x2e, 0x69, 0x73, 0x74, 0x69, 0x6f, 0x2e, 0x74, 0x65, + 0x73, 0x74, 0x42, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x5a, 0x08, 0x2e, 0x2e, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -615,29 +759,32 @@ func file_test_echo_proto_echo_proto_rawDescGZIP() []byte { return file_test_echo_proto_echo_proto_rawDescData } -var file_test_echo_proto_echo_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_test_echo_proto_echo_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_test_echo_proto_echo_proto_goTypes = []interface{}{ (*EchoRequest)(nil), // 0: proto.EchoRequest (*EchoResponse)(nil), // 1: proto.EchoResponse (*Header)(nil), // 2: proto.Header (*ForwardEchoRequest)(nil), // 3: proto.ForwardEchoRequest - (*Alpn)(nil), // 4: proto.Alpn - (*ForwardEchoResponse)(nil), // 5: proto.ForwardEchoResponse - (*wrapperspb.StringValue)(nil), // 6: google.protobuf.StringValue + (*HBONE)(nil), // 4: proto.HBONE + (*Alpn)(nil), // 5: proto.Alpn + (*ForwardEchoResponse)(nil), // 6: proto.ForwardEchoResponse + (*wrapperspb.StringValue)(nil), // 7: google.protobuf.StringValue } var file_test_echo_proto_echo_proto_depIdxs = []int32{ 2, // 0: proto.ForwardEchoRequest.headers:type_name -> proto.Header - 4, // 1: proto.ForwardEchoRequest.alpn:type_name -> proto.Alpn - 6, // 2: proto.ForwardEchoRequest.expectedResponse:type_name -> google.protobuf.StringValue - 0, // 3: proto.EchoTestService.Echo:input_type -> proto.EchoRequest - 3, // 4: proto.EchoTestService.ForwardEcho:input_type -> proto.ForwardEchoRequest - 1, // 5: proto.EchoTestService.Echo:output_type -> proto.EchoResponse - 5, // 6: proto.EchoTestService.ForwardEcho:output_type -> proto.ForwardEchoResponse - 5, // [5:7] is the sub-list for method output_type - 3, // [3:5] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 5, // 1: proto.ForwardEchoRequest.alpn:type_name -> proto.Alpn + 7, // 2: proto.ForwardEchoRequest.expectedResponse:type_name -> google.protobuf.StringValue + 4, // 3: proto.ForwardEchoRequest.hbone:type_name -> proto.HBONE + 2, // 4: proto.HBONE.headers:type_name -> proto.Header + 0, // 5: proto.EchoTestService.Echo:input_type -> proto.EchoRequest + 3, // 6: proto.EchoTestService.ForwardEcho:input_type -> proto.ForwardEchoRequest + 1, // 7: proto.EchoTestService.Echo:output_type -> proto.EchoResponse + 6, // 8: proto.EchoTestService.ForwardEcho:output_type -> proto.ForwardEchoResponse + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_test_echo_proto_echo_proto_init() } @@ -695,7 +842,7 @@ func file_test_echo_proto_echo_proto_init() { } } file_test_echo_proto_echo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Alpn); i { + switch v := v.(*HBONE); i { case 0: return &v.state case 1: @@ -707,6 +854,18 @@ func file_test_echo_proto_echo_proto_init() { } } file_test_echo_proto_echo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Alpn); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_test_echo_proto_echo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ForwardEchoResponse); i { case 0: return &v.state @@ -725,7 +884,7 @@ func file_test_echo_proto_echo_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_test_echo_proto_echo_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/test/echo/proto/echo.proto b/pkg/test/echo/proto/echo.proto index 7e931b13b99a..7d488b266cc8 100644 --- a/pkg/test/echo/proto/echo.proto +++ b/pkg/test/echo/proto/echo.proto @@ -82,6 +82,26 @@ message ForwardEchoRequest { bool newConnectionPerRequest = 22; // If set, each request will force a DNS lookup. Only applies if newConnectionPerRequest is set. bool forceDNSLookup = 23; + + // HBONE communication settings. If provided, requests will be tunnelled. + HBONE hbone = 24; +} + +message HBONE { + string address = 9; + repeated Header headers = 1; + // If non-empty, make the request with the corresponding cert and key. + string cert = 2; + string key = 3; + // If non-empty, verify the server CA + string caCert = 4; + // If non-empty, make the request with the corresponding cert and key file. + string certFile = 5; + string keyFile = 6; + // If non-empty, verify the server CA with the ca cert file. + string caCertFile = 7; + // Skip verifying peer's certificate. + bool insecureSkipVerify = 8; } message Alpn { diff --git a/pkg/test/echo/server/endpoint/hbone.go b/pkg/test/echo/server/endpoint/hbone.go new file mode 100644 index 000000000000..da93d9b9603d --- /dev/null +++ b/pkg/test/echo/server/endpoint/hbone.go @@ -0,0 +1,99 @@ +// 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 endpoint + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + + "istio.io/istio/pkg/hbone" +) + +var _ Instance = &connectInstance{} + +type connectInstance struct { + Config + server *http.Server +} + +func newHBONE(config Config) Instance { + return &connectInstance{ + Config: config, + } +} + +func (c connectInstance) Close() error { + if c.server != nil { + return c.server.Close() + } + return nil +} + +func (c connectInstance) Start(onReady OnReadyFunc) error { + defer onReady() + c.server = hbone.NewServer() + + var listener net.Listener + var port int + var err error + if c.Port.TLS { + cert, cerr := tls.LoadX509KeyPair(c.TLSCert, c.TLSKey) + if cerr != nil { + return fmt.Errorf("could not load TLS keys: %v", cerr) + } + config := &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"h2"}, + GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) { + // There isn't a way to pass through all ALPNs presented by the client down to the + // HTTP server to return in the response. However, for debugging, we can at least log + // them at this level. + epLog.Infof("TLS connection with alpn: %v", info.SupportedProtos) + return nil, nil + }, + } + // Listen on the given port and update the port if it changed from what was passed in. + listener, port, err = listenOnAddressTLS(c.ListenerIP, c.Port.Port, config) + // Store the actual listening port back to the argument. + c.Port.Port = port + } else { + // Listen on the given port and update the port if it changed from what was passed in. + listener, port, err = listenOnAddress(c.ListenerIP, c.Port.Port) + // Store the actual listening port back to the argument. + c.Port.Port = port + } + if err != nil { + return err + } + + if c.Port.TLS { + c.server.Addr = fmt.Sprintf(":%d", port) + epLog.Infof("Listening HBONE on %v\n", port) + } else { + c.server.Addr = fmt.Sprintf(":%d", port) + epLog.Infof("Listening HBONE (plaintext) on %v\n", port) + } + go func() { + err := c.server.Serve(listener) + epLog.Warnf("Port %d listener terminated with error: %v", port, err) + }() + return nil +} + +func (c connectInstance) GetConfig() Config { + return c.Config +} diff --git a/pkg/test/echo/server/endpoint/http.go b/pkg/test/echo/server/endpoint/http.go index e4547348c648..6d81a944ea4f 100644 --- a/pkg/test/echo/server/endpoint/http.go +++ b/pkg/test/echo/server/endpoint/http.go @@ -121,13 +121,13 @@ func (s *httpInstance) Start(onReady OnReadyFunc) error { } if s.isUDS() { - fmt.Printf("Listening HTTP/1.1 on %v\n", s.UDSServer) + epLog.Infof("Listening HTTP/1.1 on %v\n", s.UDSServer) } else if s.Port.TLS { s.server.Addr = fmt.Sprintf(":%d", port) - fmt.Printf("Listening HTTPS/1.1 on %v\n", port) + epLog.Infof("Listening HTTPS/1.1 on %v\n", port) } else { s.server.Addr = fmt.Sprintf(":%d", port) - fmt.Printf("Listening HTTP/1.1 on %v\n", port) + epLog.Infof("Listening HTTP/1.1 on %v\n", port) } // Start serving HTTP traffic. @@ -211,13 +211,7 @@ type codeAndSlices struct { func (h *httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { id := uuid.New() - remoteAddr, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - epLog.Warnf("failed to get host from remote address: %s", err) - } - epLog.WithLabels( - "remoteAddr", remoteAddr, "method", r.Method, "url", r.URL, "host", r.Host, "headers", r.Header, "id", id, - ).Infof("HTTP Request") + epLog.WithLabels("method", r.Method, "url", r.URL, "host", r.Host, "headers", r.Header, "id", id).Infof("HTTP Request") if h.Port == nil { defer common.Metrics.HTTPRequests.With(common.PortLabel.Value("uds")).Increment() } else { diff --git a/pkg/test/echo/server/endpoint/instance.go b/pkg/test/echo/server/endpoint/instance.go index f6a0c7de1679..80876e3d9ae2 100644 --- a/pkg/test/echo/server/endpoint/instance.go +++ b/pkg/test/echo/server/endpoint/instance.go @@ -54,6 +54,8 @@ type Instance interface { func New(cfg Config) (Instance, error) { if cfg.Port != nil { switch cfg.Port.Protocol { + case protocol.HBONE: + return newHBONE(cfg), nil case protocol.HTTP, protocol.HTTPS: return newHTTP(cfg), nil case protocol.HTTP2, protocol.GRPC: diff --git a/pkg/test/echo/server/endpoint/tcp.go b/pkg/test/echo/server/endpoint/tcp.go index abaab0d49c13..53030a14d0d7 100644 --- a/pkg/test/echo/server/endpoint/tcp.go +++ b/pkg/test/echo/server/endpoint/tcp.go @@ -75,9 +75,9 @@ func (s *tcpInstance) Start(onReady OnReadyFunc) error { s.l = listener if s.Port.TLS { - fmt.Printf("Listening TCP (over TLS) on %v\n", port) + epLog.Infof("Listening TCP (over TLS) on %v\n", port) } else { - fmt.Printf("Listening TCP on %v\n", port) + epLog.Infof("Listening TCP on %v\n", port) } // Start serving TCP traffic. diff --git a/pkg/test/echo/server/forwarder/config.go b/pkg/test/echo/server/forwarder/config.go index 33fc746fa939..2673a613e3e9 100644 --- a/pkg/test/echo/server/forwarder/config.go +++ b/pkg/test/echo/server/forwarder/config.go @@ -55,6 +55,10 @@ type Config struct { urlPath string method string secure bool + + hboneTLSConfig *tls.Config + hboneClientConfig func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) + hboneHeaders http.Header } func (c *Config) fillDefaults() error { @@ -62,6 +66,7 @@ func (c *Config) fillDefaults() error { c.timeout = common.GetTimeout(c.Request) c.count = common.GetCount(c.Request) c.headers = common.GetHeaders(c.Request) + c.hboneHeaders = common.ProtoToHTTPHeaders(c.Request.Hbone.GetHeaders()) // Extract the host from the headers and then remove it. c.hostHeader = c.headers.Get(hostHeader) @@ -91,6 +96,15 @@ func (c *Config) fillDefaults() error { if err != nil { return err } + c.hboneClientConfig, err = getHBONEClientConfig(c.Request.Hbone) + if err != nil { + return err + } + + c.hboneTLSConfig, err = newHBONETLSConfig(c) + if err != nil { + return err + } // Parse the proxy if specified. if len(c.Proxy) > 0 { @@ -183,6 +197,60 @@ func getClientCertificateFunc(r *proto.ForwardEchoRequest) (func(info *tls.Certi return nil, nil } +func getHBONEClientConfig(r *proto.HBONE) (func(info *tls.CertificateRequestInfo) (*tls.Certificate, error), error) { + if r == nil { + return nil, nil + } + if r.KeyFile != "" && r.CertFile != "" { + certData, err := os.ReadFile(r.CertFile) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate: %v", err) + } + r.Cert = string(certData) + keyData, err := os.ReadFile(r.KeyFile) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate key: %v", err) + } + r.Key = string(keyData) + } + + if r.Cert != "" && r.Key != "" { + cert, err := tls.X509KeyPair([]byte(r.Cert), []byte(r.Key)) + if err != nil { + return nil, fmt.Errorf("failed to parse x509 key pair: %v", err) + } + + for _, c := range cert.Certificate { + cert, err := x509.ParseCertificate(c) + if err != nil { + fwLog.Errorf("Failed to parse client certificate: %v", err) + } + fwLog.Debugf("Using client certificate [%s] issued by %s", cert.SerialNumber, cert.Issuer) + for _, uri := range cert.URIs { + fwLog.Debugf(" URI SAN: %s", uri) + } + } + // nolint: unparam + return func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) { + fwLog.Debugf("Peer asking for client certificate") + for i, ca := range info.AcceptableCAs { + x := &pkix.RDNSequence{} + if _, err := asn1.Unmarshal(ca, x); err != nil { + fwLog.Errorf("Failed to decode AcceptableCA[%d]: %v", i, err) + } else { + name := &pkix.Name{} + name.FillFromRDNSequence(x) + fwLog.Debugf(" AcceptableCA[%d]: %s", i, name) + } + } + + return &cert, nil + }, nil + } + + return nil, nil +} + func newTLSConfig(c *Config) (*tls.Config, error) { r := c.Request tlsConfig := &tls.Config{ @@ -238,6 +306,34 @@ func newTLSConfig(c *Config) (*tls.Config, error) { return tlsConfig, nil } +func newHBONETLSConfig(c *Config) (*tls.Config, error) { + r := c.Request.Hbone + if r == nil { + return nil, nil + } + tlsConfig := &tls.Config{ + GetClientCertificate: c.hboneClientConfig, + } + if r.CaCertFile != "" { + certData, err := os.ReadFile(r.CaCertFile) + if err != nil { + return nil, fmt.Errorf("failed to load client certificate: %v", err) + } + r.CaCert = string(certData) + } + if r.InsecureSkipVerify || r.CaCert == "" { + tlsConfig.InsecureSkipVerify = true + } else if r.CaCert != "" { + certPool := x509.NewCertPool() + if !certPool.AppendCertsFromPEM([]byte(r.CaCert)) { + return nil, fmt.Errorf("failed to create cert pool") + } + tlsConfig.RootCAs = certPool + } + + return tlsConfig, nil +} + func checkRedirectFunc(req *proto.ForwardEchoRequest) func(req *http.Request, via []*http.Request) error { if req.FollowRedirects { return nil diff --git a/pkg/test/echo/server/forwarder/grpc.go b/pkg/test/echo/server/forwarder/grpc.go index c8bb23ccf438..164abe3b9723 100644 --- a/pkg/test/echo/server/forwarder/grpc.go +++ b/pkg/test/echo/server/forwarder/grpc.go @@ -142,11 +142,10 @@ func newGRPCConnection(cfg *Config) (*grpc.ClientConn, error) { security = grpc.WithTransportCredentials(insecure.NewCredentials()) } - forceDNSLookup := cfg.forceDNSLookup opts := []grpc.DialOption{ grpc.WithAuthority(cfg.hostHeader), grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { - return newDialer(forceDNSLookup).DialContext(ctx, "tcp", addr) + return newDialer(cfg).DialContext(ctx, "tcp", addr) }), security, } diff --git a/pkg/test/echo/server/forwarder/http.go b/pkg/test/echo/server/forwarder/http.go index fd95dc5abbd7..4122ff5e9c78 100644 --- a/pkg/test/echo/server/forwarder/http.go +++ b/pkg/test/echo/server/forwarder/http.go @@ -31,6 +31,7 @@ import ( "github.com/lucas-clemente/quic-go/http3" "golang.org/x/net/http2" + "istio.io/istio/pkg/hbone" "istio.io/istio/pkg/test/echo" "istio.io/istio/pkg/test/echo/common/scheme" "istio.io/istio/pkg/test/echo/proto" @@ -107,7 +108,7 @@ func newHTTP2TransportGetter(cfg *Config) (httpTransportGetter, func()) { return &http2.Transport{ TLSClientConfig: cfg.tlsConfig, DialTLS: func(network, addr string, tlsConfig *tls.Config) (net.Conn, error) { - return tls.DialWithDialer(newDialer(cfg.forceDNSLookup), network, addr, tlsConfig) + return hbone.TLSDialWithDialer(newDialer(cfg), network, addr, tlsConfig) }, } } @@ -119,7 +120,7 @@ func newHTTP2TransportGetter(cfg *Config) (httpTransportGetter, func()) { AllowHTTP: true, // Pretend we are dialing a TLS endpoint. (Note, we ignore the passed tls.Config) DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) { - return newDialer(cfg.forceDNSLookup).Dial(network, addr) + return newDialer(cfg).Dial(network, addr) }, } } @@ -147,11 +148,11 @@ func newHTTP2TransportGetter(cfg *Config) (httpTransportGetter, func()) { func newHTTPTransportGetter(cfg *Config) (httpTransportGetter, func()) { newConn := func() *http.Transport { dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { - return newDialer(cfg.forceDNSLookup).DialContext(ctx, network, addr) + return newDialer(cfg).DialContext(ctx, network, addr) } if len(cfg.UDS) > 0 { dialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - return newDialer(cfg.forceDNSLookup).DialContext(ctx, "unix", cfg.UDS) + return newDialer(cfg).DialContext(ctx, "unix", cfg.UDS) } } out := &http.Transport{ diff --git a/pkg/test/echo/server/forwarder/tcp.go b/pkg/test/echo/server/forwarder/tcp.go index 71e3955ee6b6..019183560c18 100644 --- a/pkg/test/echo/server/forwarder/tcp.go +++ b/pkg/test/echo/server/forwarder/tcp.go @@ -18,13 +18,13 @@ import ( "bufio" "bytes" "context" - "crypto/tls" "fmt" "io" "net" "net/http" "strings" + "istio.io/istio/pkg/hbone" "istio.io/istio/pkg/test/echo" "istio.io/istio/pkg/test/echo/common" "istio.io/istio/pkg/test/echo/proto" @@ -134,10 +134,10 @@ func newTCPConnection(cfg *Config) (net.Conn, error) { address := cfg.Request.Url[len(cfg.scheme+"://"):] if cfg.secure { - return tls.DialWithDialer(newDialer(cfg.forceDNSLookup), "tcp", address, cfg.tlsConfig) + return hbone.TLSDialWithDialer(newDialer(cfg), "tcp", address, cfg.tlsConfig) } ctx, cancel := context.WithTimeout(context.Background(), common.ConnectionTimeout) defer cancel() - return newDialer(cfg.forceDNSLookup).DialContext(ctx, "tcp", address) + return newDialer(cfg).DialContext(ctx, "tcp", address) } diff --git a/pkg/test/echo/server/forwarder/tls.go b/pkg/test/echo/server/forwarder/tls.go index 6015ddf40e8d..a9defdea2f41 100644 --- a/pkg/test/echo/server/forwarder/tls.go +++ b/pkg/test/echo/server/forwarder/tls.go @@ -22,6 +22,7 @@ import ( "strings" "time" + "istio.io/istio/pkg/hbone" "istio.io/istio/pkg/test/echo" "istio.io/istio/pkg/test/echo/proto" ) @@ -117,7 +118,7 @@ func (c *tlsProtocol) Close() error { func newTLSConnection(cfg *Config) (*tls.Conn, error) { address := cfg.Request.Url[len(cfg.scheme+"://"):] - con, err := tls.DialWithDialer(newDialer(cfg.forceDNSLookup), "tcp", address, cfg.tlsConfig) + con, err := hbone.TLSDialWithDialer(newDialer(cfg), "tcp", address, cfg.tlsConfig) if err != nil { return nil, err } diff --git a/pkg/test/echo/server/forwarder/util.go b/pkg/test/echo/server/forwarder/util.go index 95afda4cba0f..c9cc564bdcd4 100644 --- a/pkg/test/echo/server/forwarder/util.go +++ b/pkg/test/echo/server/forwarder/util.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/go-multierror" + "istio.io/istio/pkg/hbone" "istio.io/istio/pkg/test/echo" "istio.io/istio/pkg/test/echo/common" "istio.io/istio/pkg/test/echo/proto" @@ -45,11 +46,19 @@ func writeForwardedHeaders(out *bytes.Buffer, requestID int, header http.Header) } } -func newDialer(forceDNSLookup bool) *net.Dialer { +func newDialer(cfg *Config) hbone.Dialer { + if cfg.Request.Hbone.GetAddress() != "" { + out := hbone.NewDialer(hbone.Config{ + ProxyAddress: cfg.Request.Hbone.GetAddress(), + Headers: cfg.hboneHeaders, + TLS: cfg.hboneTLSConfig, + }) + return out + } out := &net.Dialer{ Timeout: common.ConnectionTimeout, } - if forceDNSLookup { + if cfg.forceDNSLookup { out.Resolver = newResolver(common.ConnectionTimeout, "", "") } return out @@ -77,10 +86,6 @@ func newResolver(timeout time.Duration, protocol, dnsServer string) *net.Resolve // doForward sends the requests and collect the responses. func doForward(ctx context.Context, cfg *Config, e *executor, doReq func(context.Context, *Config, int) (string, error)) (*proto.ForwardEchoResponse, error) { - if err := cfg.fillDefaults(); err != nil { - return nil, err - } - // make the timeout apply to the entire set of requests ctx, cancel := context.WithTimeout(ctx, cfg.timeout) defer cancel() diff --git a/pkg/test/echo/server/forwarder/websocket.go b/pkg/test/echo/server/forwarder/websocket.go index 1ccff2023d41..939322656908 100644 --- a/pkg/test/echo/server/forwarder/websocket.go +++ b/pkg/test/echo/server/forwarder/websocket.go @@ -65,11 +65,11 @@ func (c *websocketProtocol) makeRequest(ctx context.Context, cfg *Config, reques } dialContext := func(network, addr string) (net.Conn, error) { - return newDialer(cfg.forceDNSLookup).Dial(network, addr) + return newDialer(cfg).Dial(network, addr) } if len(cfg.UDS) > 0 { dialContext = func(network, addr string) (net.Conn, error) { - return newDialer(cfg.forceDNSLookup).Dial("unix", cfg.UDS) + return newDialer(cfg).Dial("unix", cfg.UDS) } } diff --git a/pkg/test/echo/server/instance.go b/pkg/test/echo/server/instance.go index fe60e431c979..67cb33de553a 100644 --- a/pkg/test/echo/server/instance.go +++ b/pkg/test/echo/server/instance.go @@ -255,6 +255,7 @@ func (s *Instance) validate() error { case protocol.HTTPS: case protocol.HTTP2: case protocol.GRPC: + case protocol.HBONE: default: return fmt.Errorf("protocol %v not currently supported", port.Protocol) }