Skip to content

Commit

Permalink
TLS state is now printed to the output for ECH-enabled connections.
Browse files Browse the repository at this point in the history
In addition to that, much more TLS-related information is printed to the output
including information about TLS certificates.

Closes #8
  • Loading branch information
ameshkov committed Sep 22, 2023
1 parent 4d5edd9 commit fc767d9
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 53 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ adheres to [Semantic Versioning][semver].

## [Unreleased]

### Fixed

* TLS state is now printed to the output for ECH-enabled connections. In
addition to that, much more TLS-related information is printed to the output
including information about TLS certificates. ([#8][#8])

[#8]: https://github.com/ameshkov/gocurl/issues/8

[unreleased]: https://github.com/ameshkov/gocurl/compare/v1.1.0...HEAD

## [1.1.0] - 2023-09-21
Expand Down
58 changes: 34 additions & 24 deletions internal/client/dialer.go → internal/client/clientdialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ type clientDialer struct {
tlsConfig *tls.Config
resolver *resolve.Resolver
dial dialer.DialFunc

// conn is the last established connection via the dialer. It can be a TLS
// connection if DialTLSContext was used.
//
// TODO(ameshkov): handle QUIC connections.
conn net.Conn
}

// newDialer creates a new instance of the clientDialer.
Expand Down Expand Up @@ -59,38 +65,21 @@ func (d *clientDialer) DialTLSContext(_ context.Context, network, addr string) (
}

if d.cfg.ECH {
return d.handshakeECH(conn)
d.conn, err = d.handshakeECH(conn)
} else {
d.conn, err = d.handshakeTLS(conn)
}

return d.handshakeTLS(conn)
return d.conn, err
}

// DialContext implements proxy.ContextDialer for *clientDialer.
func (d *clientDialer) DialContext(_ context.Context, network, addr string) (c net.Conn, err error) {
d.out.Debug("Connecting to %s", addr)

return d.dial(network, addr)
}
d.conn, err = d.dial(network, addr)

// handshakeTLS attempts to establish a TLS connection.
func (d *clientDialer) handshakeTLS(conn net.Conn) (tlsConn net.Conn, err error) {
tlsClient := tls.Client(conn, d.tlsConfig)
err = tlsClient.Handshake()
if err != nil {
return nil, err
}

return tlsClient, nil
}

// handshakeECH attempts to establish a ECH-enabled TLS connection.
func (d *clientDialer) handshakeECH(conn net.Conn) (tlsConn net.Conn, err error) {
echConfigs, err := d.resolver.LookupECHConfigs(d.tlsConfig.ServerName)
if err != nil {
return nil, err
}

return ech.HandshakeECH(conn, echConfigs, d.tlsConfig, d.out)
return d.conn, err
}

// DialQUIC establishes a new QUIC connection and is supposed to be used by
Expand All @@ -100,7 +89,7 @@ func (d *clientDialer) DialQUIC(
addr string,
_ *tls.Config,
cfg *quic.Config,
) (quic.EarlyConnection, error) {
) (c quic.EarlyConnection, err error) {
conn, err := d.dial("udp", addr)
if err != nil {
return nil, err
Expand All @@ -119,6 +108,27 @@ func (d *clientDialer) DialQUIC(
return quic.DialEarly(ctx, uConn, udpAddr, d.tlsConfig, cfg)
}

// handshakeTLS attempts to establish a TLS connection.
func (d *clientDialer) handshakeTLS(conn net.Conn) (tlsConn net.Conn, err error) {
tlsClient := tls.Client(conn, d.tlsConfig)
err = tlsClient.Handshake()
if err != nil {
return nil, err
}

return tlsClient, nil
}

// handshakeECH attempts to establish a ECH-enabled TLS connection.
func (d *clientDialer) handshakeECH(conn net.Conn) (tlsConn net.Conn, err error) {
echConfigs, err := d.resolver.LookupECHConfigs(d.tlsConfig.ServerName)
if err != nil {
return nil, err
}

return ech.HandshakeECH(conn, echConfigs, d.tlsConfig, d.out)
}

// createDialFunc creates dialFunc that implements all the logic configured by
// cfg.
func createDialFunc(
Expand Down
87 changes: 86 additions & 1 deletion internal/client/ech/ech.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package ech
import (
"crypto/tls"
"net"
"time"

ctls "github.com/ameshkov/cfcrypto/tls"
"github.com/ameshkov/gocurl/internal/output"
Expand Down Expand Up @@ -46,5 +47,89 @@ func HandshakeECH(

out.Debug("ECH-enabled connection has been established successfully")

return c, nil
return &connWrapper{
baseConn: c,
}, nil
}

// tlsConnectionStater is an interface that declares ConnectionState function
// of tls.Conn. The reason for implementing this is to allow HTTP client to
// get access to the TLS connection state and expose it via http.Response.TLS
type tlsConnectionStater interface {
ConnectionState() (state tls.ConnectionState)
}

// connWrapper is a wrapper over *ctls.Conn that implements tlsConnectionStater
// interface and provides a way for HTTP client to get access to TLS properties
// of the connection.
type connWrapper struct {
*tls.Conn

// baseConn
baseConn *ctls.Conn
}

// type check
var _ net.Conn = (*connWrapper)(nil)

// type check
var _ tlsConnectionStater = (*connWrapper)(nil)

// ConnectionState implements the tlsConnectionStater for *connWrapper.
func (c *connWrapper) ConnectionState() (state tls.ConnectionState) {
innerState := c.baseConn.ConnectionState()

state.Version = innerState.Version
state.NegotiatedProtocol = innerState.NegotiatedProtocol
state.ServerName = innerState.ServerName
state.CipherSuite = innerState.CipherSuite
state.DidResume = innerState.DidResume
state.HandshakeComplete = innerState.HandshakeComplete
state.OCSPResponse = innerState.OCSPResponse
state.PeerCertificates = innerState.PeerCertificates
state.SignedCertificateTimestamps = innerState.SignedCertificateTimestamps
state.TLSUnique = innerState.TLSUnique
state.VerifiedChains = innerState.VerifiedChains

return state
}

// Read implements the net.Conn interface for *connWrapper.
func (c *connWrapper) Read(b []byte) (n int, err error) {
return c.baseConn.Read(b)
}

// Write implements the net.Conn interface for *connWrapper.
func (c *connWrapper) Write(b []byte) (n int, err error) {
return c.baseConn.Write(b)
}

// Close implements the net.Conn interface for *connWrapper.
func (c *connWrapper) Close() (err error) {
return c.baseConn.Close()
}

// LocalAddr implements the net.Conn interface for *connWrapper.
func (c *connWrapper) LocalAddr() (addr net.Addr) {
return c.baseConn.LocalAddr()
}

// RemoteAddr implements the net.Conn interface for *connWrapper.
func (c *connWrapper) RemoteAddr() (addr net.Addr) {
return c.baseConn.RemoteAddr()
}

// SetDeadline implements the net.Conn interface for *connWrapper.
func (c *connWrapper) SetDeadline(t time.Time) (err error) {
return c.baseConn.SetDeadline(t)
}

// SetReadDeadline implements the net.Conn interface for *connWrapper.
func (c *connWrapper) SetReadDeadline(t time.Time) (err error) {
return c.baseConn.SetReadDeadline(t)
}

// SetWriteDeadline implements the net.Conn interface for *connWrapper.
func (c *connWrapper) SetWriteDeadline(t time.Time) (err error) {
return c.baseConn.SetWriteDeadline(t)
}
41 changes: 36 additions & 5 deletions internal/client/client.go → internal/client/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package client

import (
"crypto/tls"
"net/http"

"github.com/ameshkov/gocurl/internal/config"
Expand All @@ -10,21 +11,51 @@ import (
"golang.org/x/net/http2"
)

// NewClient creates a new *http.Client based on *cmd.Options.
func NewClient(cfg *config.Config, out *output.Output) (client *http.Client, err error) {
c := &http.Client{}
// transport is a wrapper over regular http.RoundTripper that is used to add
// additional logic on top of RoundTrip.
type transport struct {
d *clientDialer
base http.RoundTripper
}

// type check
var _ http.RoundTripper = (*transport)(nil)

// RoundTrip implements the http.RoundTripper interface for *transport.
func (t *transport) RoundTrip(r *http.Request) (resp *http.Response, err error) {
resp, err = t.base.RoundTrip(r)
if err != nil {
return nil, err
}

// Make sure that resp.TLS field is set regardless of what protocol was
// used. This is important for ECH-enabled connections as crypto/tls is
// not used there.
type tlsConnectionStater interface {
ConnectionState() tls.ConnectionState
}
if c, ok := t.d.conn.(tlsConnectionStater); ok {
state := c.ConnectionState()
resp.TLS = &state
}

return resp, err
}

// NewTransport creates a new http.RoundTripper that will be used for making
// the request.
func NewTransport(cfg *config.Config, out *output.Output) (rt http.RoundTripper, err error) {
d, err := newDialer(cfg, out)
if err != nil {
return nil, err
}

c.Transport, err = createHTTPTransport(d, cfg)
bt, err := createHTTPTransport(d, cfg)
if err != nil {
return nil, err
}

return c, nil
return &transport{d: d, base: bt}, nil
}

// createHTTPTransport creates http.RoundTripper that will be used by the
Expand Down
16 changes: 3 additions & 13 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
package cmd

import (
"crypto/tls"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -44,9 +43,9 @@ func Main() {

out.Debug("Starting gocurl %s with arguments:\n%s", version.Version(), cfg.RawOptions)

c, err := client.NewClient(cfg, out)
transport, err := client.NewTransport(cfg, out)
if err != nil {
out.Info("Failed to create client: %v", err)
out.Info("Failed to create HTTP transport: %v", err)

os.Exit(1)
}
Expand All @@ -66,22 +65,13 @@ func Main() {
cloneReq, _ := client.NewRequest(cfg)
out.DebugRequest(cloneReq)

resp, err := c.Do(req)
resp, err := transport.RoundTrip(req)
if err != nil {
out.Info("Failed to make request: %v", err)

os.Exit(1)
}

if resp.TLS != nil {
out.Debug("TLS version: %s", tls.VersionName(resp.TLS.Version))
out.Debug("TLS cipher suite: %s", tls.CipherSuiteName(resp.TLS.CipherSuite))

if resp.TLS.NegotiatedProtocol != "" {
out.Debug("TLS negotiated protocol: %s", resp.TLS.NegotiatedProtocol)
}
}

out.DebugResponse(resp)

defer func(r io.ReadCloser) {
Expand Down
Loading

0 comments on commit fc767d9

Please sign in to comment.