Created
October 18, 2020 00:32
-
-
Save x893675/1fbf72acc4630453fdab02df824c4337 to your computer and use it in GitHub Desktop.
http2 server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"crypto/tls" | |
"fmt" | |
"golang.org/x/net/http2" | |
"golang.org/x/sys/unix" | |
"io" | |
"io/ioutil" | |
"log" | |
"net" | |
"net/http" | |
"os" | |
"strconv" | |
"strings" | |
"syscall" | |
"time" | |
) | |
func main() { | |
h := &http.ServeMux{} | |
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { | |
for k, v := range r.Header { | |
fmt.Printf("key: %s, value: %s\n", k, v) | |
} | |
w.Write([]byte(fmt.Sprintf("Protocol: %s", r.Proto))) | |
}) | |
opt := NewSecureServingOptions() | |
_ = opt.Apply() | |
s := SecureServingInfo{ | |
Listener: opt.Listener, | |
MinTLSVersion: 0, | |
CipherSuites: nil, | |
HTTP2MaxStreamsPerConnection: opt.HTTP2MaxStreamsPerConnection, | |
DisableHTTP2: false, | |
} | |
tlsConfig, err := s.tlsConfig() | |
if err != nil { | |
panic(err) | |
} | |
secureServer := &http.Server{ | |
Addr: s.Listener.Addr().String(), | |
Handler: h, | |
MaxHeaderBytes: 1 << 20, | |
TLSConfig: tlsConfig, | |
} | |
// At least 99% of serialized resources in surveyed clusters were smaller than 256kb. | |
// This should be big enough to accommodate most API POST requests in a single frame, | |
// and small enough to allow a per connection buffer of this size multiplied by `MaxConcurrentStreams`. | |
const resourceBody99Percentile = 256 * 1024 | |
http2Options := &http2.Server{} | |
// shrink the per-stream buffer and max framesize from the 1MB default while still accommodating most API POST requests in a single frame | |
http2Options.MaxUploadBufferPerStream = resourceBody99Percentile | |
http2Options.MaxReadFrameSize = resourceBody99Percentile | |
// use the overridden concurrent streams setting or make the default of 250 explicit so we can size MaxUploadBufferPerConnection appropriately | |
if s.HTTP2MaxStreamsPerConnection > 0 { | |
http2Options.MaxConcurrentStreams = uint32(s.HTTP2MaxStreamsPerConnection) | |
} else { | |
http2Options.MaxConcurrentStreams = 250 | |
} | |
// increase the connection buffer size from the 1MB default to handle the specified number of concurrent streams | |
http2Options.MaxUploadBufferPerConnection = http2Options.MaxUploadBufferPerStream * int32(http2Options.MaxConcurrentStreams) | |
if !s.DisableHTTP2 { | |
// apply settings to the server | |
if err := http2.ConfigureServer(secureServer, http2Options); err != nil { | |
panic(fmt.Errorf("error configuring http2: %v", err)) | |
} | |
} | |
// use tlsHandshakeErrorWriter to handle messages of tls handshake error | |
tlsErrorWriter := &tlsHandshakeErrorWriter{os.Stderr} | |
tlsErrorLogger := log.New(tlsErrorWriter, "", 0) | |
secureServer.ErrorLog = tlsErrorLogger | |
fmt.Printf("Serving securely on %s\n", secureServer.Addr) | |
var listener net.Listener | |
listener = tcpKeepAliveListener{s.Listener} | |
if secureServer.TLSConfig != nil { | |
listener = tls.NewListener(listener, secureServer.TLSConfig) | |
} | |
err = secureServer.ServeTLS(s.Listener, "", "") | |
if err != nil { | |
log.Fatal(err) | |
} | |
} | |
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted | |
// connections. It's used by ListenAndServe and ListenAndServeTLS so | |
// dead TCP connections (e.g. closing laptop mid-download) eventually | |
// go away. | |
// | |
// Copied from Go 1.7.2 net/http/server.go | |
type tcpKeepAliveListener struct { | |
net.Listener | |
} | |
const ( | |
defaultKeepAlivePeriod = 3 * time.Minute | |
) | |
func (ln tcpKeepAliveListener) Accept() (net.Conn, error) { | |
c, err := ln.Listener.Accept() | |
if err != nil { | |
return nil, err | |
} | |
if tc, ok := c.(*net.TCPConn); ok { | |
tc.SetKeepAlive(true) | |
tc.SetKeepAlivePeriod(defaultKeepAlivePeriod) | |
} | |
return c, nil | |
} | |
type SecureServingInfo struct { | |
// Listener is the secure server network listener. | |
Listener net.Listener | |
// Cert is the main server cert which is used if SNI does not match. Cert must be non-nil and is | |
// allowed to be in SNICerts. | |
//Cert dynamiccertificates.CertKeyContentProvider | |
// | |
//// SNICerts are the TLS certificates used for SNI. | |
//SNICerts []dynamiccertificates.SNICertKeyContentProvider | |
// | |
//// ClientCA is the certificate bundle for all the signers that you'll recognize for incoming client certificates | |
//ClientCA dynamiccertificates.CAContentProvider | |
// MinTLSVersion optionally overrides the minimum TLS version supported. | |
// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). | |
MinTLSVersion uint16 | |
// CipherSuites optionally overrides the list of allowed cipher suites for the server. | |
// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). | |
CipherSuites []uint16 | |
// HTTP2MaxStreamsPerConnection is the limit that the api server imposes on each client. | |
// A value of zero means to use the default provided by golang's HTTP/2 support. | |
HTTP2MaxStreamsPerConnection int | |
// DisableHTTP2 indicates that http2 should not be enabled. | |
DisableHTTP2 bool | |
} | |
type SecureServingOptions struct { | |
BindAddress net.IP | |
// BindPort is ignored when Listener is set, will serve https even with 0. | |
BindPort int | |
// BindNetwork is the type of network to bind to - defaults to "tcp", accepts "tcp", | |
// "tcp4", and "tcp6". | |
BindNetwork string | |
// Required set to true means that BindPort cannot be zero. | |
Required bool | |
// ExternalAddress is the address advertised, even if BindAddress is a loopback. By default this | |
// is set to BindAddress if the later no loopback, or to the first host interface address. | |
ExternalAddress net.IP | |
// Listener is the secure server network listener. | |
// either Listener or BindAddress/BindPort/BindNetwork is set, | |
// if Listener is set, use it and omit BindAddress/BindPort/BindNetwork. | |
Listener net.Listener | |
//// ServerCert is the TLS cert info for serving secure traffic | |
//ServerCert GeneratableKeyCert | |
//// SNICertKeys are named CertKeys for serving secure traffic with SNI support. | |
//SNICertKeys []cliflag.NamedCertKey | |
//// CipherSuites is the list of allowed cipher suites for the server. | |
//// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). | |
//CipherSuites []string | |
//// MinTLSVersion is the minimum TLS version supported. | |
//// Values are from tls package constants (https://golang.org/pkg/crypto/tls/#pkg-constants). | |
//MinTLSVersion string | |
// HTTP2MaxStreamsPerConnection is the limit that the api server imposes on each client. | |
// A value of zero means to use the default provided by golang's HTTP/2 support. | |
HTTP2MaxStreamsPerConnection int | |
// PermitPortSharing controls if SO_REUSEPORT is used when binding the port, which allows | |
// more than one instance to bind on the same address and port. | |
PermitPortSharing bool | |
} | |
func NewSecureServingOptions() *SecureServingOptions { | |
return &SecureServingOptions{ | |
BindAddress: net.ParseIP("0.0.0.0"), | |
BindPort: 6443, | |
BindNetwork: "", | |
Required: false, | |
ExternalAddress: nil, | |
Listener: nil, | |
HTTP2MaxStreamsPerConnection: 0, | |
PermitPortSharing: false, | |
} | |
} | |
func (s *SecureServingOptions) Apply() error { | |
if s.Listener == nil { | |
var err error | |
addr := net.JoinHostPort(s.BindAddress.String(), strconv.Itoa(s.BindPort)) | |
c := net.ListenConfig{} | |
if s.PermitPortSharing { | |
c.Control = permitPortReuse | |
} | |
s.Listener, s.BindPort, err = CreateListener(s.BindNetwork, addr, c) | |
if err != nil { | |
return fmt.Errorf("failed to create listener: %v", err) | |
} | |
} else { | |
if _, ok := s.Listener.Addr().(*net.TCPAddr); !ok { | |
return fmt.Errorf("failed to parse ip and port from listener") | |
} | |
s.BindPort = s.Listener.Addr().(*net.TCPAddr).Port | |
s.BindAddress = s.Listener.Addr().(*net.TCPAddr).IP | |
} | |
return nil | |
} | |
func CreateListener(network, addr string, config net.ListenConfig) (net.Listener, int, error) { | |
if len(network) == 0 { | |
network = "tcp" | |
} | |
ln, err := config.Listen(context.TODO(), network, addr) | |
if err != nil { | |
return nil, 0, fmt.Errorf("failed to listen on %v: %v", addr, err) | |
} | |
// get port | |
tcpAddr, ok := ln.Addr().(*net.TCPAddr) | |
if !ok { | |
ln.Close() | |
return nil, 0, fmt.Errorf("invalid listen address: %q", ln.Addr().String()) | |
} | |
return ln, tcpAddr.Port, nil | |
} | |
func permitPortReuse(network, addr string, conn syscall.RawConn) error { | |
return conn.Control(func(fd uintptr) { | |
syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, unix.SO_REUSEPORT, 1) | |
}) | |
} | |
func (s *SecureServingInfo) tlsConfig() (*tls.Config, error) { | |
tlsConfig := &tls.Config{ | |
// Can't use SSLv3 because of POODLE and BEAST | |
// Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher | |
// Can't use TLSv1.1 because of RC4 cipher usage | |
MinVersion: tls.VersionTLS12, | |
// enable HTTP2 for go's 1.7 HTTP Server | |
NextProtos: []string{"h2", "http/1.1"}, | |
} | |
// these are static aspects of the tls.Config | |
if s.DisableHTTP2 { | |
tlsConfig.NextProtos = []string{"http/1.1"} | |
} | |
if s.MinTLSVersion > 0 { | |
tlsConfig.MinVersion = s.MinTLSVersion | |
} | |
if len(s.CipherSuites) > 0 { | |
tlsConfig.CipherSuites = s.CipherSuites | |
insecureCiphers := InsecureTLSCiphers() | |
for i := 0; i < len(s.CipherSuites); i++ { | |
for cipherName, cipherID := range insecureCiphers { | |
if s.CipherSuites[i] == cipherID { | |
fmt.Printf("Use of insecure cipher '%s' detected.", cipherName) | |
} | |
} | |
} | |
} | |
// openssl genrsa -out private.key 4096 | |
// openssl req -new -x509 -sha256 -days 1825 -key private.key -out public.crt | |
crt, err := ioutil.ReadFile("./public.crt") | |
if err != nil { | |
log.Fatal(err) | |
} | |
key, err := ioutil.ReadFile("./private.key") | |
if err != nil { | |
log.Fatal(err) | |
} | |
cert, err := tls.X509KeyPair(crt, key) | |
if err != nil { | |
log.Fatal(err) | |
} | |
tlsConfig.Certificates = []tls.Certificate{cert} | |
tlsConfig.ServerName = "localhost" | |
//if s.ClientCA != nil { | |
// // Populate PeerCertificates in requests, but don't reject connections without certificates | |
// // This allows certificates to be validated by authenticators, while still allowing other auth types | |
// tlsConfig.ClientAuth = tls.RequestClientCert | |
//} | |
return tlsConfig, nil | |
} | |
var insecureCiphers = map[string]uint16{ | |
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, | |
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256, | |
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, | |
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA, | |
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, | |
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, | |
} | |
// InsecureTLSCiphers returns the cipher suites implemented by crypto/tls which have | |
// security issues. | |
func InsecureTLSCiphers() map[string]uint16 { | |
cipherKeys := make(map[string]uint16, len(insecureCiphers)) | |
for k, v := range insecureCiphers { | |
cipherKeys[k] = v | |
} | |
return cipherKeys | |
} | |
// tlsHandshakeErrorWriter writes TLS handshake errors to klog with | |
// trace level - V(5), to avoid flooding of tls handshake errors. | |
type tlsHandshakeErrorWriter struct { | |
out io.Writer | |
} | |
const tlsHandshakeErrorPrefix = "http: TLS handshake error" | |
func (w *tlsHandshakeErrorWriter) Write(p []byte) (int, error) { | |
if strings.Contains(string(p), tlsHandshakeErrorPrefix) { | |
fmt.Println(string(p)) | |
//metrics.TLSHandshakeErrors.Inc() | |
return len(p), nil | |
} | |
// for non tls handshake error, log it as usual | |
return w.out.Write(p) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment