Hello all,
I’m encountering a difficulty whereas implementing a check for SMTP STARTTLS performance. Particularly, when the TLS handshake fails, I wish to check {that a} QUIT
command is distributed earlier than gracefully closing the connection.
Subject:
- The
QUIT
command seems to get caught within the TLS connection handler’s buffer - This habits is intermittent and appears to rely upon how shortly the command is distributed
- Including a small delay earlier than sending the
QUIT
command resolves the problem
To isolate the issue, I’ve created a minimal replica case utilizing easy plain textual content communication, with out SMTP-specific code.
package deal essential
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/massive"
"internet"
"strings"
"sync"
"testing"
"time"
)
func TestTLSHandshake(t *testing.T) {
ln := newLocalListener(t)
defer ln.Shut()
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
// Consumer
clientConn, _ := internet.Dial("tcp", ln.Addr().String())
defer clientConn.Shut()
_, _ = clientConn.Write([]byte("STARTTLSrn"))
// Look ahead to readiness
buf := make([]byte, 1024)
_, _ = clientConn.Learn(buf)
fmt.Printf("[Client] Obtained reply: %s", string(buf))
// Attempt TLS handshake
tlsConn := tls.Consumer(clientConn, &tls.Config{
ServerName: "localhost",
})
// Handshake ought to fail as a result of shopper would not belief self-signed cert
err := tlsConn.Handshake()
if err != nil {
fmt.Println("[Client] Handshake err: ", err)
}
// Delay to forestall QUIT from being consumed into TLS buffer
time.Sleep(100 * time.Millisecond)
_, _ = clientConn.Write([]byte("QUITrn"))
wg.Finished()
}()
serverConn, _ := ln.Settle for()
defer serverConn.Shut()
quitReceived := false
scanner := bufio.NewScanner(serverConn)
for scanner.Scan() {
enter := scanner.Textual content()
fmt.Printf("[Server] Obtained: '%s'n", enter)
swap enter {
case "STARTTLS":
// Generate check cert
cert, privateKey, _ := generateTestCertificatePair()
// Setup TLS
keypair, _ := tls.X509KeyPair(cert, privateKey)
config := &tls.Config{Certificates: []tls.Certificates{keypair}}
// Reply readiness
_, _ = serverConn.Write([]byte("Prepared for handshakern"))
tlsConn := tls.Server(serverConn, config)
handshakeErr := tlsConn.Handshake()
if handshakeErr != nil {
fmt.Println("[Server] Handshake error: ", handshakeErr)
proceed
} else {
fmt.Println("[Server] Handshake accomplished")
t.Deadly("Handshake unexpectedly succeeded")
}
case "QUIT":
quitReceived = true
fmt.Println("[Server] Stop obtained")
}
}
wg.Wait()
if !quitReceived {
t.Deadly("[Server] Stop not obtained")
}
}
// Generate X509 encoded certificates for testing.
func generateTestCertificatePair() ([]byte, []byte, error) {
group := "instance"
host := "localhost"
validFrom := time.Now()
validTo := validFrom.Add(24 * time.Hour)
// Generate non-public key
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
// Create certificates serial quantity
serialNumberLimit := new(massive.Int).Lsh(massive.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, nil, err
}
template := x509.Certificates{
SerialNumber: serialNumber,
Topic: pkix.Title{
Group: []string{group},
CommonName: host,
},
NotBefore: validFrom,
NotAfter: validTo,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
hosts := strings.Cut up(host, ",")
template.DNSNames = append(template.DNSNames, hosts...)
// Create certificates
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, nil, err
}
// Encode certificates to PEM
certBuffer := &bytes.Buffer{}
if err := pem.Encode(certBuffer, &pem.Block{Sort: "CERTIFICATE", Bytes: derBytes}); err != nil {
return nil, nil, err
}
// Encode non-public key to PEM
privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
return nil, nil, err
}
keyBuffer := &bytes.Buffer{}
if err := pem.Encode(keyBuffer, &pem.Block{Sort: "PRIVATE KEY", Bytes: privBytes}); err != nil {
return nil, nil, err
}
return certBuffer.Bytes(), keyBuffer.Bytes(), nil
}
func newLocalListener(t *testing.T) internet.Listener {
ln, _ := internet.Pay attention("tcp", "localhost:0")
return ln
}
The above code can also be obtainable right here. Discover that if the delay is eliminated, the server is not going to obtain the QUIT
command.
I perceive that TCP is a steady stream and the appliance ought to have the message boundary by itself. Nonetheless, I’m utilizing tls.Server()
and don’t have management over the way it will deal with the TCP connection.
Is there something I can do to make sure that the server will be capable of obtain the QUIT command is obtained by the principle handler as an alternative of the TLS one?
Thanks to your assist!