pkg/hbone/hboned.go (133 lines of code) (raw):
// Copyright 2021 Google LLC
//
// 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
//
// https://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"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"sync"
"time"
"golang.org/x/net/http2"
)
// HBone represents a node using a HTTP/2 or HTTP/3 based overlay network environment.
//
// Each HBone node has a Istio (spiffee) certificate.
//
// HBone can be used as a client, server or gateway.
type HBone struct {
h2Server *http2.Server
Cert *tls.Certificate
rp *httputil.ReverseProxy
// Non-local endpoints. Key is the 'pod id' of a H2R client
Endpoints map[string]*Endpoint
// H2R holds H2 client (reverse) connections to the local server.
// Will be used to route requests directly. Key is the SNI expected in forwarding requests.
H2R map[string]http.RoundTripper
h2rListener net.Listener
sniListener net.Listener
h2t *http2.Transport
SNIAddr string
HTTPClientSystem *http.Client
HTTPClientMesh *http.Client
// Ports is the equivalent of container ports in k8s.
// Name follows the same conventions as Istio and should match the port name in the Service.
// Port "*" means 'any' port - if set, allows connections to any port by number.
// Currently this is loaded from env variables named PORT_name=value, with the default PORT_http=8080
// TODO: refine the 'wildcard' to indicate http1/2 protocol
// TODO: this can be populated from a WorkloadGroup object, loaded from XDS or mesh env.
Ports map[string]string
TokenCallback func(ctx context.Context, host string) (string, error)
Mux http.ServeMux
// Timeout used for TLS handshakes. If not set, 3 seconds is used.
HandsahakeTimeout time.Duration
EndpointResolver func(sni string) *Endpoint
m sync.RWMutex
H2RConn map[*http2.ClientConn]string
H2RCallback func(string, *http2.ClientConn)
}
// New creates a new HBone node. It requires a workload identity, including mTLS certificates.
func New() *HBone {
// 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
h2.StrictMaxConcurrentStreams = false
hb := &HBone{
Endpoints: map[string]*Endpoint{},
H2R: map[string]http.RoundTripper{},
H2RConn: map[*http2.ClientConn]string{},
h2t: h2,
Ports: map[string]string{},
//&http2.Transport{
// ReadIdleTimeout: 10000 * time.Second,
// StrictMaxConcurrentStreams: false,
// AllowHTTP: true,
//},
HTTPClientSystem: http.DefaultClient,
}
//hb.h2t.ConnPool = hb
hb.h2Server = &http2.Server{}
u, _ := url.Parse("http://127.0.0.1:8080")
hb.rp = httputil.NewSingleHostReverseProxy(u)
return hb
}
type HBoneAcceptedConn struct {
hb *HBone
conn net.Conn
}
// StartBHoneD will listen on addr as H2C (typically :15009)
//
//
// Incoming streams for /_hbone/mtls will be treated as a mTLS connection,
// using the Istio certificates and root. After handling mTLS, the clear text
// connection will be forwarded to localhost:8080 ( TODO: custom port ).
//
// TODO: setting for app protocol=h2, http, tcp - initial impl uses tcp
//
// Incoming requests for /_hbone/22 will be forwarded to localhost:22, for
// debugging with ssh.
//
func (hac *HBoneAcceptedConn) ServeHTTP(w http.ResponseWriter, r *http.Request) {
t0 := time.Now()
var proxyErr error
defer func() {
if r := recover(); r != nil {
switch x := r.(type) {
case error:
proxyErr = x
}
}
log.Println("hbone", "url", r.URL, "host", r.Host, "remote", r.RemoteAddr,
"dur", time.Since(t0), "err", proxyErr)
}()
// TODO: parse Envoy / hbone headers.
if strings.HasPrefix(r.RequestURI, "/_hbone/") {
// Force the headers to be sent.
w.(http.Flusher).Flush()
portName := r.RequestURI[8:]
switch portName {
case "15003":
// Default mTLS port.
proxyErr = hac.hb.HandleTCPProxy(w, r.Body, "127.0.0.1:15003")
return
case "22":
// TCP proxy for SSH ( no mTLS, SSH has its own equivalent)
proxyErr = hac.hb.HandleTCPProxy(w, r.Body, "127.0.0.1:15022")
return
}
val := hac.hb.Ports[portName]
if val != "" {
proxyErr = hac.hb.HandleTCPProxy(w, r.Body, val)
return
}
w.WriteHeader(404)
return
}
// This is not a tunnel, but regular request.
// Make sure xfcc header is removed
r.Header.Del("x-forwarded-client-cert")
hac.hb.rp.ServeHTTP(w, r)
}
func (hb *HBone) HandleAcceptedH2C(conn net.Conn) {
hc := &HBoneAcceptedConn{hb: hb, conn: conn}
hb.h2Server.ServeConn(
conn,
&http2.ServeConnOpts{
Handler: hc, // Also plain text, needs to be upgraded
Context: context.Background(),
//Context can be used to cancel, pass meta.
// h2 adds http.LocalAddrContextKey(NetAddr), ServerContextKey (*Server)
})
}
// HandleTCPProxy connects and forwards r/w to the hostPort
func (hb *HBone) HandleTCPProxy(w io.Writer, r io.Reader, hostPort string) error {
nc, err := net.Dial("tcp", hostPort)
if err != nil {
log.Println("Error dialing ", hostPort, err)
return err
}
s1 := Stream{
ID: "TCP-o",
Dst: nc,
Src: r,
}
ch := make(chan int)
go s1.CopyBuffered(ch, true)
s2 := Stream{
ID: "TCP-i",
Dst: w,
Src: nc,
}
s2.CopyBuffered(nil, true)
<-ch
if s1.Err != nil {
return s1.Err
}
if s2.Err != nil {
return s2.Err
}
return nil
}