Skip to content
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

net/http: EOF during TLS handshake #61721

Open
go-aegian opened this issue Aug 2, 2023 · 7 comments
Open

net/http: EOF during TLS handshake #61721

go-aegian opened this issue Aug 2, 2023 · 7 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.

Comments

@go-aegian
Copy link

The reported EOF error, happens when tls.ClientAuth is set to tls.VerifyClientCertIfGiven - if no certificate is provided or request comes from any browser it throws this error, and it should not given that only if Cert if given, unless there is a way to setup the browser to send a specific client cert?

File - /src/net/http/server.go

Current version -

if err := tlsConn.HandshakeContext(ctx); err != nil {
			// If the handshake failed due to the client not speaking
			// TLS, assume they're speaking plaintext HTTP and write a
			// 400 response on the TLS conn's underlying net.Conn.
			if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
				io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
				re.Conn.Close()
				return
			}
			c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
			return
		}

Propose Fix - tested on my environment - add this to the if line - && err.Error() != "EOF"

if err := tlsConn.HandshakeContext(ctx); err != nil && err.Error() != "EOF" {
			// If the handshake failed due to the client not speaking
			// TLS, assume they're speaking plaintext HTTP and write a
			// 400 response on the TLS conn's underlying net.Conn.
			if re, ok := err.(tls.RecordHeaderError); ok && re.Conn != nil && tlsRecordHeaderLooksLikeHTTP(re.RecordHeader) {
				io.WriteString(re.Conn, "HTTP/1.0 400 Bad Request\r\n\r\nClient sent an HTTP request to an HTTPS server.\n")
				re.Conn.Close()
				return
			}
			c.server.logf("http: TLS handshake error from %s: %v", c.rwc.RemoteAddr(), err)
			return
		}
@mauri870
Copy link
Member

mauri870 commented Aug 2, 2023

You could use err != nil && err != io.EOF. Not sure tho if this is okay to change. Do you have a reproducible example to share?

@mauri870
Copy link
Member

mauri870 commented Aug 2, 2023

Please change the title to net/http: EOF during TLS handshake

I feel like an EOF can't be ignored here, but hard to say without knowing the context to reproduce this. Likely the connection was refused for a reason, maybe the client asked for a protocol that the server does not support.

If possible share the Go version you are using alongside with a reproducible test case.

Might be related to #13523, #50984

@go-aegian go-aegian changed the title http: TLS handshake error from 127.0.0.1:xxx: EOF net/http: EOF during TLS handshake Aug 2, 2023
@go-aegian
Copy link
Author

Changed title.

Yes that will work too - err != nil && err != io.EOF

Here is an example to run the server on https://localhost:8300/test - open a new browser session after starting the server as it only happens on handshake not when opening a new browser tab after hitting the url

create a certificates folder and add in there a ca.crt, localhost.crt and localhost.key that you can generate on your end


package main

import (
	"crypto/rand"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
	
	"github.com/pkg/errors"
	"golang.org/x/net/http2"
)

var Ciphers = []uint16{
	// TLS 1.3
	tls.TLS_AES_256_GCM_SHA384,
	tls.TLS_AES_128_GCM_SHA256,
	tls.TLS_CHACHA20_POLY1305_SHA256,
	
	// ECDSA is about 3 times faster than RSA on the server side.
	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
	tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
	tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
	
	// RSA is slower on the server side but still widely used.
	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
	tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
}
var (
	HTTP11    = "http/1.1"
	HTTP2     = "h2"
	ALPNProto = "acme-tls/1"
)

func main() {
	caFile := "certificates/ca.crt"
	caCert, errReadFile := readFile(caFile, "CA Cert")
	if errReadFile != nil {
		log.Fatalf("failed to read ca certificate %s: %v", caFile, errReadFile)
	}
	caCertPool, _ := x509.SystemCertPool()
	if caCertPool == nil {
		caCertPool = x509.NewCertPool()
	}
	if !caCertPool.AppendCertsFromPEM(caCert) {
		log.Fatalf("failed to append CA certificate %s", caFile)
	}
	
	serverCert, _ := getServerCert()
	tlsConfig := &tls.Config{
		MinVersion:       tls.VersionTLS13,
		MaxVersion:       tls.VersionTLS13,
		CipherSuites:     Ciphers,
		NextProtos:       []string{HTTP2, HTTP11, ALPNProto},
		CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
		Certificates:     []tls.Certificate{*serverCert},
		ClientAuth:       tls.VerifyClientCertIfGiven,
		Rand:             rand.Reader,
		RootCAs:          caCertPool,
		ClientCAs:        caCertPool,
	}
	
	tcpListener, _ := tls.Listen("tcp", ":8300", tlsConfig)
	
	s2 := &http2.Server{
		MaxHandlers:                  0,
		MaxConcurrentStreams:         0,
		MaxDecoderHeaderTableSize:    0,
		MaxEncoderHeaderTableSize:    0,
		MaxReadFrameSize:             0,
		PermitProhibitedCipherSuites: true,
		IdleTimeout:                  10 * time.Second,
		MaxUploadBufferPerConnection: 65535,
		MaxUploadBufferPerStream:     1,
		NewWriteScheduler:            nil,
		CountError:                   nil,
	}
	router := http.NewServeMux()
	router.HandleFunc("/test", func(w http.ResponseWriter, req *http.Request) {
		_, _ = fmt.Fprintf(w, "Welcome to the home page!")
	})
	server := &http.Server{
		Handler:           router,
		Addr:              fmt.Sprintf("%s:%d", "localhost", 8300),
		ReadHeaderTimeout: 120 * time.Second,
		WriteTimeout:      120 * time.Second,
		IdleTimeout:       120 * time.Second,
		ReadTimeout:       120 * time.Second,
		TLSConfig:         tlsConfig,
		MaxHeaderBytes:    1048576,
		ErrorLog:          log.New(os.Stderr, "", 0),
	}
	
	if err := http2.ConfigureServer(server, s2); err != nil {
		panic(err)
	}
	
	if err := server.Serve(tcpListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
		panic(err)
	}
}

func getServerCert() (*tls.Certificate, error) {
	cert := "certificates/localhost.crt"
	
	certBytes, err := readFile(cert, "Cert")
	if err != nil {
		return nil, errors.Errorf("failed to read certificate %s: %v", cert, err)
	}
	
	key := "certificates/localhost.key"
	certKeyBytes, err := readFile(key, "Key")
	if err != nil {
		return nil, errors.Errorf("failed to read certificate key %s: %v", key, err)
	}
	
	serverCert, errCert := tls.X509KeyPair(certBytes, certKeyBytes)
	if errCert != nil {
		return nil, errors.Errorf("failed to load certificate %s and key %s: %v", cert, key, errCert)
	}
	
	return &serverCert, nil
}

func readFile(file, errorPrefix string) ([]byte, error) {
	file = strings.TrimSpace(file)
	if file == "" {
		return nil, errors.Errorf("%s file cannot be blank", errorPrefix)
	}
	
	osf, err := os.Stat(file)
	if err != nil {
		return nil, errors.Errorf("%s file not found %s: %v", errorPrefix, file, err)
	}
	
	if osf.IsDir() {
		return nil, errors.Errorf("%s file needs to specify a file in its path", errorPrefix)
	}
	if osf.Size() < 1 {
		return nil, errors.Errorf("%s file cannot be empty", errorPrefix)
	}
	
	bytes, err := os.ReadFile(file)
	if err != nil {
		return nil, errors.Errorf("failed to read %s file %s: %v", errorPrefix, file, err)
	}
	
	if len(bytes) < 1 {
		return nil, errors.Errorf("%s file %s is empty", errorPrefix, file)
	}
	
	return bytes, nil
}

@mauri870
Copy link
Member

mauri870 commented Aug 2, 2023

I think it might have to do with the ciphers being offered by the client (browser in this case). If I try to access the page with firefox I also get the EOF.

If I try to offer a supported cipher there is no EOF error: openssl s_client -connect localhost:8300 -ciphersuites TLS_AES_128_GCM_SHA256 -prexit (TLSv1.3, Cipher is TLS_AES_128_GCM_SHA256)

@go-aegian
Copy link
Author

If you add all ciphers still gets an EOF. What is Firefox, Chrome, Edge sending as cipher and what client cert they send?

var Ciphers = []uint16{
// TLS 1.3
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,

// ECDSA is about 3 times faster than RSA on the server side.
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,

// RSA is slower on the server side but still widely used.
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

// Added so all ciphers are available

tls.TLS_RSA_WITH_RC4_128_SHA,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
tls.TLS_FALLBACK_SCSV,

}

@dr2chase
Copy link
Contributor

dr2chase commented Aug 3, 2023

@golang/security @neild
Not sure if this is a security question or a net question.

@dr2chase dr2chase added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Aug 3, 2023
@wangyuanwenGit
Copy link

I also have the same problem occur.
I implemented a proxy server using net.http and also used terminal TLS, but I also got EOF.
And I captured the packet through wireshark, and what I saw was that the client actively closed the tcp connection (that is, sent a FIN), and then I thought that http.server just read an EOF.
This is very confusing. I can't determine what the problem is. If I access the same URL on different computers, I may not be able to get this EOF.
I have seen a lot of discussions about https EOF. Can you provide some ideas or solutions and reasons? Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

4 participants