1
0
mirror of https://github.com/matrix-org/matrix-federation-tester.git synced 2025-09-17 05:02:14 +03:00
Files
federation-tester/main.go
Richard van der Hoff ded612f2d9 Update gomatrixserverlib version (#155)
Update to a more recent version of gomatrixserverlib, mostly to pick up the fix to matrix-org/gomatrixserverlib#440.

Since GMSL now requires go 1.18, bump to that.

Fixes #153.
2024-11-12 11:05:10 +00:00

702 lines
25 KiB
Go

package main
import (
"bytes"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
_ "net/http/pprof" // nolint:gosec
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/fclient"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/matrix-org/matrix-federation-tester/promutils"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/collectors"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
const (
fetchKeysTimeout = 10 * time.Second
)
type ResponseType = int64
const (
ResponseTypeJSON ResponseType = iota
ResponseTypeText
)
func main() {
lookupFlag := flag.String("lookup", "", "Name of homeserver to look up, instead of starting an HTTP server")
flag.Parse()
if len(*lookupFlag) > 0 {
printReport(*lookupFlag)
} else {
runHTTPServer()
}
}
func printReport(serverName string) {
ctx := context.Background()
report, err := Report(ctx, spec.ServerName(serverName))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to generate report: %v\n", err)
os.Exit(1)
}
json, err := report.toJSON()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to serialize report: %v\n", err)
os.Exit(1)
}
os.Stdout.Write(json)
os.Stdout.WriteString("\n")
}
// HandleReport handles an HTTP request for a JSON report for a matrix server.
// Example request: GET /api/report?server_name=matrix.org request.
func HandleReport(w http.ResponseWriter, req *http.Request) {
handleCommon(w, req, ResponseTypeJSON)
}
// HandleFederationOk handles an HTTP request for a boolean text report for a matrix server.
// Example request: GET /api/federation-ok?server_name=matrix.org
func HandleFederationOk(w http.ResponseWriter, req *http.Request) {
handleCommon(w, req, ResponseTypeText)
}
// Write common headers, make sure the incoming request is valid and finally
// write the response depending on the `ResponseType` accordingly.
func handleCommon(w http.ResponseWriter, req *http.Request, rt ResponseType) {
// Set unrestricted Access-Control headers so that this API can be used by
// web apps running in browsers.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept")
if req.Method == "OPTIONS" {
return
}
if req.Method != "GET" {
w.WriteHeader(405)
handleRequestError(w, "Unsupported method")
return
}
serverName := spec.ServerName(req.URL.Query().Get("server_name"))
if len(serverName) == 0 {
w.WriteHeader(400)
handleRequestError(w, "Missing server_name parameter")
return
}
result, err := Report(req.Context(), serverName)
if err != nil {
w.WriteHeader(500)
handleRequestError(w, fmt.Sprintf("Error generating report: %s\n", err.Error()))
return
}
if rt == ResponseTypeJSON {
err := writeJSONResponse(w, result)
if err != nil {
handleRequestError(w, fmt.Sprintf("Error writing response: %s\n", err.Error()))
}
} else if rt == ResponseTypeText {
writeTextResponse(w, result)
}
}
// handleRequestError prints an error message to the standard output then tries
// to write it to a http.ResponseWriter.
// If writing failed, prints a message containing the error that came up then to
// the standard output.
func handleRequestError(w http.ResponseWriter, errMsg string) {
fmt.Printf("ERR: %s\n", errMsg)
if _, err := w.Write([]byte(errMsg)); err != nil {
fmt.Printf("Error sending error to client: %s\n", err.Error())
}
}
// Convert report to JSON bytes and write it as response.
func writeJSONResponse(
w http.ResponseWriter,
report ServerReport,
) error {
json, err := report.toJSON()
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
if _, err = w.Write(json); err != nil {
fmt.Printf("Error writing response to client: %s\n", err.Error())
}
return nil
}
// Write `GOOD` or `BAD` as response depending on the `report.FederationOK` bool.
func writeTextResponse(
w http.ResponseWriter,
report ServerReport,
) {
text := ""
if report.FederationOK {
text = "GOOD"
} else {
text = "BAD"
}
response := []byte(text)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(200)
_, err := w.Write(response)
if err != nil {
fmt.Printf("Error writing response to client: %s\n", err.Error())
}
}
// runHTTPServer starts an HTTP daemon, serving requests for federation tests. Never exits.
func runHTTPServer() {
// Create a new Prometheus registry, because I can't figure out how to get a
// handle to the default registry
registry := prometheus.NewPedanticRegistry()
// reinstall the usual metrics
registry.MustRegister(
collectors.NewGoCollector(),
collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
)
// create a middleware for instrumenting our application endpoints with
middleware := promutils.NewInstrumentationMiddleware(registry)
// Register our application endpoints, wrapped with Prometheus instrumentation
http.HandleFunc("/api/report", middleware.NewHandler("report", http.HandlerFunc(HandleReport)))
http.HandleFunc("/api/federation-ok", middleware.NewHandler("federation-ok", http.HandlerFunc(HandleFederationOk)))
// Serve Prometheus metrics on /metrics
http.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{}))
server := &http.Server{
Addr: os.Getenv("BIND_ADDRESS"),
ReadHeaderTimeout: 3 * time.Second,
}
// ListenAndServe always returns a non-nil error so we want to panic here.
panic(server.ListenAndServe())
}
// A ServerReport is a report for a matrix server.
type ServerReport struct {
Error string `json:",omitempty"` // Error which happened before connecting to the server.
WellKnownResult WellKnownReport // The result of looking up the server's .well-known/matrix/server file.
DNSResult DNSResult // The result of looking up the server in DNS.
ConnectionReports map[string]ConnectionReport // The report for each server address we could connect to.
ConnectionErrors map[string]error // The errors for each server address we couldn't connect to.
Version VersionReport // The version information for the server
FederationOK bool // Summary about whether the run didn't encounter anything that could hamper federation.
}
// A VersionReport is a combination of data from matrix server's version
// information, as well as any errors reported during the lookup.
type VersionReport struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Error string `json:"error,omitempty"`
}
// A WellKnownReport is the combination of data from a matrix server's
// .well-known file, as well as any errors reported during the lookup.
type WellKnownReport struct {
ServerAddress spec.ServerName `json:"m.server"`
Result string `json:"result,omitempty"`
CacheExpiresAt int64
}
// A DNSResult is the result of looking up a matrix server in DNS.
type DNSResult struct {
SRVSkipped bool // True if the SRV record lookup was skipped due to the server name or .well-known target including a port number.
SRVCName string // The canonical name for the SRV record in DNS
SRVRecords []*net.SRV // List of SRV record for the matrix server.
SRVError error // If there was an error getting the SRV records.
Hosts map[string]HostResult // The results of looking up the SRV record targets.
Addrs []string // List of "<ip>:<port>" strings that the server is listening on. These strings can be passed to `net.Dial()`.
}
// A HostResult is the result of looking up the IP addresses for a host.
type HostResult struct {
CName string // The canonical name for the host.
Addrs []string // The IP addresses for the host.
Error error // If there was an error getting the IP addresses.
}
// Info is a struct that contains federation checks that are not necessary in
// order for proper federation. These are placed in a separate field in order to
// make parsing the resulting JSON simpler
type Info struct{}
// A ConnectionReport is information about a connection made to a matrix server.
type ConnectionReport struct {
Certificates []X509CertSummary // Summary information for each x509 certificate served up by this server.
Cipher CipherSummary // Summary information on the TLS cipher used by this server.
Checks ConnectionChecks // Checks applied to the server and their results.
Errors []error // String slice describing any problems encountered during testing.
Ed25519VerifyKeys map[gomatrixserverlib.KeyID]spec.Base64Bytes // The Verify keys for this server or nil if the checks were not ok.
Info Info // Checks that are not necessary to pass, rather simply informative.
Keys *json.RawMessage // The server key JSON returned by this server.
}
// ConnectionChecks represents the result of the checks done on a connection
// made to a Matrix homeserver. It extends the gomatrixserverlib.KeyChecks
// structure.
type ConnectionChecks struct {
gomatrixserverlib.KeyChecks // Checks done by gomatrixserverlib from the keys exposed by the server.
ValidCertificates bool // The X509 certificates have been verified by the system root CAs.
}
// A CipherSummary is a summary of the TLS version and Cipher used in a TLS connection.
type CipherSummary struct {
Version string // Human readable description of the TLS version.
CipherSuite string // Human readable description of the TLS cipher.
}
// A X509CertSummary is a summary of the information in a X509 certificate.
type X509CertSummary struct {
SubjectCommonName string // The common name of the subject.
IssuerCommonName string // The common name of the issuer.
SHA256Fingerprint spec.Base64Bytes // The SHA256 fingerprint of the certificate.
DNSNames []string // The DNS names this certificate is valid for.
}
// Report creates a ServerReport for a matrix server.
func Report(
ctx context.Context,
serverName spec.ServerName,
) (report ServerReport, err error) {
// Map of network address to report.
report.ConnectionReports = make(map[string]ConnectionReport)
// Map of network address to connection error.
report.ConnectionErrors = make(map[string]error)
// This would be set to false as soon as one check fails or an error is reported.
// TODO: We probably should expect it to be false and only set it to true if everything
// worked after checking.
report.FederationOK = true
// Host address of the server (can be different from the serverName through well-known)
serverHost := serverName
// Validate the server name, and retrieve domain name to send as SNI to server
sni, _, valid := spec.ParseAndValidateServerName(serverHost)
if !valid {
report.Error = fmt.Sprintf("Invalid server name '%s'", serverHost)
report.FederationOK = false
return
}
// Check for .well-known
var wellKnownResult *fclient.WellKnownResult
if wellKnownResult, err = fclient.LookupWellKnown(ctx, serverName); err == nil {
// Use well-known as new host
serverHost = wellKnownResult.NewAddress
report.WellKnownResult.ServerAddress = wellKnownResult.NewAddress
report.WellKnownResult.CacheExpiresAt = wellKnownResult.CacheExpiresAt
// need to revalidate the server name and update the SNI
sni, _, valid = spec.ParseAndValidateServerName(serverHost)
if !valid {
report.Error = fmt.Sprintf("Invalid server name '%s' in .well-known result", serverHost)
report.FederationOK = false
return
}
} else {
report.WellKnownResult.Result = err.Error()
}
// Lookup server version
client := fclient.NewClient(
fclient.WithWellKnownSRVLookups(true),
)
version, err := client.GetVersion(ctx, serverName)
if err == nil {
report.Version.Name = version.Server.Name
report.Version.Version = version.Server.Version
} else {
report.Version.Error = err.Error()
report.FederationOK = false
}
dnsResult, err := lookupServer(serverHost)
if err != nil {
return
}
report.DNSResult = *dnsResult
// Mark federation as not OK if no address could be found.
if len(report.DNSResult.Addrs) == 0 {
report.FederationOK = false
}
// Ensure only one thread updates the report at a time.
mutex := new(sync.Mutex)
wg := sync.WaitGroup{}
// Iterate through each address and run checks in parallel
for _, addr := range report.DNSResult.Addrs {
wg.Add(1)
go func(report *ServerReport, serverHost, serverName spec.ServerName, addr, sni string) {
defer wg.Done()
if connReport, connErr := connCheck(
ctx, addr, serverHost, serverName, sni,
); connErr != nil {
mutex.Lock()
defer mutex.Unlock()
report.FederationOK = false
report.ConnectionErrors[addr] = connErr
} else {
mutex.Lock()
defer mutex.Unlock()
report.FederationOK = report.FederationOK && connReport.Checks.AllChecksOK
report.ConnectionReports[addr] = *connReport
}
}(&report, serverHost, serverName, addr, sni)
}
// Wait for checks to finish
wg.Wait()
return
}
// lookupServer looks up a matrix server in DNS.
func lookupServer(serverName spec.ServerName) (*DNSResult, error) { // nolint: gocyclo
var result DNSResult
result.Hosts = map[string]HostResult{}
hosts := map[string][]net.SRV{}
if !strings.Contains(string(serverName), ":") {
// If there isn't an explicit port set then try to look up the SRV record.
var err error
result.SRVCName, result.SRVRecords, err = net.LookupSRV("matrix", "tcp", string(serverName))
result.SRVError = err
if err != nil {
if dnserr, ok := err.(*net.DNSError); ok {
// If the error is a network timeout talking to the DNS server
// then give up now rather than trying to fallback.
if dnserr.Timeout() {
return nil, err
}
}
// If there isn't a SRV record in DNS then fallback to "serverName:8448".
hosts[string(serverName)] = []net.SRV{{
Target: string(serverName),
Port: 8448,
}}
} else {
// Group the SRV records by target host.
for _, record := range result.SRVRecords {
// Check whether the target is a CNAME record.
cname, err := net.LookupCNAME(record.Target)
if err != nil {
result.Hosts[record.Target] = HostResult{
CName: cname,
Error: err,
}
continue
}
// There is no straightforward way to know whether a the target
// is an A record or a CNAME one. Therefore, we use the fact
// that LookupCNAME returns the FQDN it was given if it can't
// find a CNAME record to follow.
if cname != record.Target {
result.Hosts[record.Target] = HostResult{
CName: cname,
Error: fmt.Errorf("SRV record target %s is a CNAME record, which is forbidden (as per RFC2782)", record.Target),
}
continue
}
hosts[record.Target] = append(hosts[record.Target], *record)
}
}
} else {
// There is a explicit port set in the server name.
// We don't need to look up any SRV records.
host, portStr, err := net.SplitHostPort(string(serverName))
if err != nil {
return nil, err
}
var port uint64
port, err = strconv.ParseUint(portStr, 10, 16)
if err != nil {
return nil, err
}
hosts[host] = []net.SRV{{
Target: host,
Port: uint16(port),
}}
result.SRVSkipped = true
}
// Look up the IP addresses for each host.
for host, records := range hosts {
// Ignore any DNS errors when looking up the CNAME. We only are interested in it for debugging.
cname, err := net.LookupCNAME(host)
if err != nil {
continue
}
addrs, err := net.LookupHost(host)
result.Hosts[host] = HostResult{
CName: cname,
Addrs: addrs,
Error: err,
}
// For each SRV record, for each IP address add a "<ip>:<port>" entry to the list of addresses.
for _, record := range records {
for _, addr := range addrs {
ipPort := net.JoinHostPort(addr, strconv.Itoa(int(record.Port)))
result.Addrs = append(result.Addrs, ipPort)
}
}
}
return &result, nil
}
// connCheck generates a connection report for a given address.
// It's given the address to generate a report for, the server's host (which can
// differ from the server's name if .well-known delegation is in use, and can be
// either a single hostname or a hostname and a port), the server's name, the
// SNI to send to the server when talking to it (which is the hostname part of
// serverHost), and the result of a .well-known lookup.
// Returns an error if the keys for the server couldn't be fetched.
func connCheck(
ctx context.Context,
addr string, serverHost, serverName spec.ServerName, sni string,
) (*ConnectionReport, error) {
keys, connState, err := fetchKeysDirect(ctx, serverHost, addr, sni)
if err != nil {
return nil, err
}
var connReport = new(ConnectionReport)
// Slice of human readable errors found during testing.
connReport.Errors = make([]error, 0, 0)
// Check for valid X509 certificate
// We can assume connState.PeerCertificates[0] exists because tls returns an
// error if the message contained 0 certificates, cf
// https://golang.org/src/crypto/tls/handshake_client.go#L445
leafCert := connState.PeerCertificates[0]
intermediateCerts := x509.NewCertPool()
for _, cert := range connState.PeerCertificates[1:] {
intermediateCerts.AddCert(cert)
}
valid, err := isValidCertificate(serverHost, leafCert, intermediateCerts)
if err != nil {
connReport.Errors = append(connReport.Errors, asReportError(err))
}
connReport.Checks.ValidCertificates = valid
for _, cert := range connState.PeerCertificates {
fingerprint := sha256.Sum256(cert.Raw)
summary := X509CertSummary{
SubjectCommonName: cert.Subject.CommonName,
IssuerCommonName: cert.Issuer.CommonName,
SHA256Fingerprint: fingerprint[:],
DNSNames: cert.DNSNames,
}
connReport.Certificates = append(connReport.Certificates, summary)
}
connReport.Cipher.Version = enumToString(tlsVersions, connState.Version)
connReport.Cipher.CipherSuite = enumToString(tlsCipherSuites, connState.CipherSuite)
connReport.Checks.KeyChecks, connReport.Ed25519VerifyKeys = gomatrixserverlib.CheckKeys(serverName, time.Now(), *keys)
// Certificate validity verification isn't done by CheckKeys so we need to
// make sure AllChecksOK is false if it failed.
connReport.Checks.AllChecksOK = connReport.Checks.AllChecksOK && connReport.Checks.ValidCertificates
connReport.Info = infoChecks()
raw := json.RawMessage(keys.Raw)
connReport.Keys = &raw
return connReport, nil
}
// isValidCertificate checks if the given x509 certificate can be verified using
// system root CAs and an optional pool of intermediate CAs.
func isValidCertificate(serverName spec.ServerName, c *x509.Certificate, intermediates *x509.CertPool) (valid bool, err error) {
host, _, isValid := spec.ParseAndValidateServerName(serverName)
if !isValid {
err = fmt.Errorf("%q is not a valid serverName", serverName)
return false, err
}
// Check certificate chain validity
verificationOpts := x509.VerifyOptions{
// Certificate.Verify appears to handle IP addresses optionally surrounded by square brackets.
DNSName: host,
Intermediates: intermediates,
}
roots, err := c.Verify(verificationOpts)
return len(roots) > 0, err
}
// fetchKeysDirect fetches the matrix keys for a given server name directly from
// the given address.
// Optionally sets a SNI header if “sni“ is not empty.
// Note that this function doesn't check the validity of the certificate(s)
// served by the server.
// Returns an error if either sending the request or decoding the JSON response
// failed. The server responding with a non-200 response also causes an error to
// be returned.
// Returns the server keys and the state of the TLS connection used to retrieve
// them.
func fetchKeysDirect(
ctx context.Context,
serverName spec.ServerName, addr, sni string,
) (*gomatrixserverlib.ServerKeys, *tls.ConnectionState, error) {
cli := http.Client{
Timeout: fetchKeysTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
ServerName: sni,
// TODO: Remove this once Synapse 1.0 is out.
InsecureSkipVerify: true, // nolint: gas, gosec
},
},
}
// Create a GET /_matrix/key/v2/server request.
requestURL := "https://" + addr + "/_matrix/key/v2/server"
request, err := http.NewRequestWithContext(ctx, "GET", requestURL, nil)
if err != nil {
return nil, nil, err
}
request.Host = string(serverName)
request.Header.Set("Connection", "close")
// Send the request and wait for the response.
response, err := cli.Do(request)
if err != nil {
return nil, nil, err
}
if response != nil {
defer response.Body.Close() // nolint: errcheck
}
if response.StatusCode != http.StatusOK {
return nil, nil, fmt.Errorf("Non-200 response %d from remote server", response.StatusCode)
}
var keys gomatrixserverlib.ServerKeys
if err = json.NewDecoder(response.Body).Decode(&keys); err != nil {
return nil, nil, errors.Wrap(err, "Unable to decode JSON from remote server")
}
return &keys, response.TLS, nil
}
// infoChecks are checks that are not required for federation, just good-to-knows
func infoChecks() Info {
return Info{}
}
// A ReportError is a version of a golang error that is human readable when serialised as JSON.
type ReportError struct {
Message string // The result of err.Error()
}
// Error implements the error interface.
func (e ReportError) Error() string {
return e.Message
}
// Replace a golang error with an error that is human readable when serialised as JSON.
func asReportError(err error) error {
if err != nil {
return ReportError{err.Error()}
}
return nil
}
// touchUpReport converts all the errors in a ServerReport into forms that will be human readable after JSON serialisation.
func (report *ServerReport) touchUpReport() {
report.DNSResult.SRVError = asReportError(report.DNSResult.SRVError)
for host, hostReport := range report.DNSResult.Hosts {
hostReport.Error = asReportError(hostReport.Error)
report.DNSResult.Hosts[host] = hostReport
}
for addr, err := range report.ConnectionErrors {
report.ConnectionErrors[addr] = asReportError(err)
}
}
// toJSON serializes the ServerReport to JSON.
func (report *ServerReport) toJSON() ([]byte, error) {
report.touchUpReport()
encoded, err := json.Marshal(report)
if err != nil {
return nil, err
}
var buffer bytes.Buffer
if err = json.Indent(&buffer, encoded, "", " "); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// enumToString converts a uint16 enum into a human readable string using a fixed mapping.
// If no mapping can be found then return a "UNKNOWN[0x%x]" string with the raw enum.
func enumToString(names map[uint16]string, value uint16) string {
if name, ok := names[value]; ok {
return name
}
return fmt.Sprintf("UNKNOWN[0x%x]", value)
}
var (
tlsVersions = map[uint16]string{
tls.VersionSSL30: "SSL 3.0",
tls.VersionTLS10: "TLS 1.0",
tls.VersionTLS11: "TLS 1.1",
tls.VersionTLS12: "TLS 1.2",
tls.VersionTLS13: "TLS 1.3",
}
tlsCipherSuites = map[uint16]string{
tls.TLS_RSA_WITH_RC4_128_SHA: "TLS_RSA_WITH_RC4_128_SHA",
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_CBC_SHA: "TLS_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_RSA_WITH_AES_256_CBC_SHA: "TLS_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: "TLS_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: "TLS_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: "TLS_ECDHE_ECDSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: "TLS_ECDHE_RSA_WITH_RC4_128_SHA",
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA",
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
tls.TLS_AES_128_GCM_SHA256: "TLS_AES_128_GCM_SHA256",
tls.TLS_AES_256_GCM_SHA384: "TLS_AES_256_GCM_SHA384",
tls.TLS_CHACHA20_POLY1305_SHA256: "TLS_CHACHA20_POLY1305_SHA256",
}
)