-
Notifications
You must be signed in to change notification settings - Fork 8k
hbone: initial echo server/client implementation #39645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will HBONE always be over h2? If we ever supported h3, would it be considered a different protocol? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the future we may make it support h3. TBD if we consider that another protocol here. This is just internal details here, not API, so we can change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we not add it here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We would have to update all of our tests and echo codebase. Any concerns ? It cannot be used by real Service (which I plan to add a comment and test asserting this) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My concern is test code should not couple with main. Could be confused at a new protocol? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think "experimental" is more accurate than "test". This only exists in this file. Seems that an adequate "proceed at your own risk" banner should be sufficient, no? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well it cannot be used by users at all, its purely an internal thing |
||
// Unsupported - value to signify that the protocol is unsupported. | ||
Unsupported Instance = "UnsupportedProtocol" | ||
) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would skip 'over mTLS', and add 'over HTTP/2 and H3 CONNECT' ( not clear if QUIC is mTLS, or if in a trusted networks we must have outer mTLS or H2C is allowed ) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now, isn't it mTLS? I'd rather keep the text representative of what has been built rather than what might be built in the future. |
||
|
||
## Specification | ||
|
||
TODO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My doc on gateway improvements has several links, you can add them here ( RFCs, BeyondCorp, browser specs) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 ... could you fill out this section? |
||
|
||
## 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) | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
howardjohn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we need to work on this - see my previous implementation, the HTTP RT should probably be done part of Dial, with proxy remaining in background. So we can return errors properly ( similar with how normal Dial works - i.e. a real connection/stream will be established by dial) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed and also made it reuse the transport within a dialer |
||
} | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need this ? Normal pattern is create Dialer with config and call DialContext/Dial There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure another way to add TLS to a Dialer? |
||
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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we have a dedicated port ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or 2 ports actually - one for HBone and one for 'plain text/trusted network' hbone, i.e. h2 or h2c
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not something a user can configure by portname on Service -- but used for internal. Not by convention, this is enforced.