关注网络安全
和行业未来

使用Go 1.8优化net/http和crypto/tls

本文由cloudflareGopher Academy advent series特别撰写。20161226重新发布在Cloudflare blog中。

crypto/tls还很慢而net/http刚出现的时候,一般的做法是将Go服务器隐藏在一个反向代理服务器(如NGINX)后面。而今天我们不再需要这样做了!

最近,我们在Cloudflare上做了一项试验,把Go服务完全暴露在充满敌意的区域网络中。在Go 1.8版本下,net/httpcrypto/tls被证明是稳定、高效而灵活的。

但是,默认设置需要根据本地服务进行调整。在本文中我们会看到怎样调优Go服务器以使其在互联网环境中更加坚固。

crypto/tls

现在都2017了,你还在运行不安全的HTTP服务器吗?现在流行的crypto/tls,又快又安全!

默认设置就像Mozilla指南中推荐的“中间”配置。但你仍应设置PreferServerCipherSuites以确保启用更加安全和快速的密码套件,同时设置CurvePreferences以避免未优化的曲线:一个使用CurveP384的客户端可能会多消耗1秒的CPU时间。

&tls.Config{

    // Causes servers to use Go's default ciphersuite preferences,

    // which are tuned to avoid attacks. Does nothing on clients.

    PreferServerCipherSuites: true,

    // Only use curves which have assembly implementations

    CurvePreferences: []tls.CurveID{

        tls.CurveP256,

        tls.X25519, // Go 1.8 only

    },

}

    如果你能接受新配置带来的兼容性方面的损失,那么你还应该设置MinVersionCipherSuites

MinVersion: tls.VersionTLS12,

    CipherSuites: []uint16{

        tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,

        tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,

        tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 only

        tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,   // Go 1.8 only

        tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,

        tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,

        // Best disabled, as they don't provide Forward Secrecy,

        // but might be necessary for some clients

        // tls.TLS_RSA_WITH_AES_256_GCM_SHA384,

        // tls.TLS_RSA_WITH_AES_128_GCM_SHA256,

},

要知道,尽管Go 1.8中已采取了部分措施,CBC密码套件的Go实现(我们在上面的“现代”模式中已将其禁用)仍易受到Lucky13攻击。

此外,需要注意的是,所有这些建议只针对amd64架构,在该架构下,快速的恒定时间加密算法(AES-GCM、ChaCha20-Poly1305、P256)也是可行的。其他架构很可能无法在实际中使用。

由于该服务器将被暴露在互联网中,它需要一张被公认的可信证书。此外,不要忘记将HTTP页面重定向至HTTPS,如果你的客户端是浏览器的话,可以考虑使用HSTS

srv := &http.Server{ 

    ReadTimeout:  5 * time.Second,

    WriteTimeout: 5 * time.Second,

    Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

        w.Header().Set("Connection", "close")

        url := "https://" + req.Host + req.URL.String()

        http.Redirect(w, req, url, http.StatusMovedPermanently)

    }),

}

go func() { log.Fatal(srv.ListenAndServe()) }() 

你可以使用SSL Tools来检查配置是否正确。

net/http

net/http是一个成熟的HTTP/1.1和HTTP/2协议栈,在这里我们将讨论服务器端及其背后的情况。

响应超时

响应超时可能是最容易被忽视的参数。如果忽略了它,你的服务在可控的网络中可能运行得还可以,但在开放的互联网中就不行了,特别是(但不只是)受到恶意攻击的时候。

响应超时的应用是一种资源控制。即便Go例程很便宜,文件描述符依旧受限。一个不再工作或被恶意延迟的连接不应该占用它们。

当服务器的文件描述符不够用时,就无法接受新的连接并出现如下报错

http: Accept error: accept tcp [::]:80: accept: too many open files; retrying in 1s 

例如,像http.ListenAndServehttp.ListenAndServeTLS使用的那种默认的http.Server就是设置不了超时的,自然也就不符合要求。

    这里有3种主要超时出现在http.Server中:ReadTimeoutWriteTimeoutIdleTimeout。你可以通过服务器设置它们:

srv := &http.Server{ 

    ReadTimeout:  5 * time.Second,

    WriteTimeout: 10 * time.Second,

    IdleTimeout:  120 * time.Second,

    TLSConfig:    tlsConfig,

    Handler:      serveMux,

}

log.Println(srv.ListenAndServeTLS("", "")) 

ReadTimeout 覆盖从连接被接受一直到所有请求的body被完全读取的全部时间(如果读取body就要完整地读取,否则就读取到header结束)。它是在net/http中通过连接被接受后立即调用SetReadDeadline 来执行的。

ReadTimeout 的问题是,它不允许服务器给客户端更多时间来根据路径或内容以流的方式处理一个请求的body。Go 1.8引入了ReadHeaderTimeout,它只覆盖了请求的header。但是,从Handler端仍然没有一种明确的方式来处理超时。在#16100议题中会讨论不同的设计方案。

WriteTimeout通常覆盖从读取请求的header的结尾到写入响应的结尾这段时间(这段时间也叫ServeHTTP的生存期),它是通过在readRequest 的末尾调用SetWriteDeadline函数实现的。

但是,当连接是HTTPS时,SetWriteDeadline会在连接被接受后立刻调用一次,以便它也覆盖作为TLS握手的一部分写入的包。这就导致了处理TLS的握手超时,WriteTimeout需要读取HTTP包头以及等待第一个字节传输后才结束。

ReadTimeout类似,WriteTimeout 是“绝对”的,WriteTimeout 无法从Handler中进行控制。(#16100

最后,Go 1.8引入了IdleTimeout,用来控制服务器端KeepAlive的连接允许空闲的最大时间。在Go 1.8之前,ReadTimeout有一个很大的问题,对于Keepalive的连接是不友好的(尽管可以在应用层来解决Idle的超时问题):因为在上一个请求的读取完毕后,下一个请求的ReadTimeout会立即开始重新计时,这样连接空闲的时间也算在ReadTimeout内,造成了连接的过早断开。

当处理不受信任的客户端和网络的连接时,你应该设置ReadWriteIdle,这样客户端就不会因为过慢的写或读而占用连接了。

HTTP/2

在Go 1.6及更新版本中,满足以下条件,HTTP2会自动开启:

  • 请求基于TLS/HTTPS
  • TLSNextProto设置为nil(注意,设置为空map会禁用HTTP2)
  • TLSConfig被设置且ListenAndServerTLS被使用;或者
  • 使用Serve,同时Config.NextProtos包含"h2"(例如[]string{"h2","http/1.1"}

尽管因为相同的连接可以同时服务于不同的请求,使得HTTP/2有很多作用,然而在Go中,它被抽象为同一组服务器超时。

不幸的是,ReadTimeout 在Go 1.7中会使HTTP/2连接断开,不再为每个请求重置,而是只能在连接开始时就设置好,每次ReadTimeout 结束之后就会打断所有的HTTP/2连接。这一缺陷在Go 1.8版本中已修复。因此,你应该尽快将服务器升级到1.8版本。

TCP Keep-Alives

如果你在用ListenAndServe(与此相对的是给Serve传一个net.Listener参数,但是这种方式没有做任何防护),那么三分钟长的TCP keepalive时间将自动被设置。这是为了防止客户端莫名其妙消失后,连接再也不会被关闭的bug。但不要相信这个,无论如何还是要设置响应超时。如果嫌三分钟太长,你可以修改tcpKeepAliveListener

由于一个只保证客户端仍有响应但却不对连接时长设上限的Keep-Alive可以被一直占用,一个恶意客户端可以打开和你的服务器的文件标识符一样多的连接,并通过header一直占用它们,不对keep-alive做出回应,从而瘫痪你的服务。

综上所述,我认为设置响应超时对于防止链接外泄极为重要

ServeMux

http.Handle[Func]或部分web框架这类的包级别函数,可以在全局http.DefaultServeMux(如果Server.Handlernil)上注册handler。而这种情况应该避免。因为任何你导入的包都会直接或通过其他依赖关系获得http.DefaultServeMux访问权限,并可能注册你预料之外的路径。

例如,如果某个包导入net/http/pprof,客户端就能得到你的应用程序使用CPU的概况。你仍可以通过手动注册它的handler来使用net/http/pprof

相反,你可以自己在http.ServeMux上注册handler并将其设置为Server.Handler。或将你的web框架设置为Server.Handler

Logging

net/http在让出你的handler的控制权之前可以做很多事情:接受连接、运行TLS握手等。

Server.ErrorLog用于记录所有问题。其中一些像响应超时和重设连接之类的问题,就会出现在互联网上。尽管不是那么清晰,但你可以中途截取其中大部分信息并通过Logger Writer的正则表达式将它们变为metrics。这保证了:每个日志操作都会产生一个单独的写方法调用。

要从handler内部中止而不记录堆栈跟踪信息,你可以使用panicnil或在Go 1.8中使用panichttp.ErrAbortHandler

Metrics

Prometheus通过使用proc文件系统来监控开放文件标识符的数量,即metrics。

如果你需要调查一个漏洞,你可以用Server.ConnState钩子从连接所在的阶段获取更多详细的metrics。但需要注意的是,不保持状态就无法保证StateActive连接计数的正确,因此你需要保证一个 map[net.Conn]ConnState

总结

如今,必须先有NGINX才能使用Go服务的日子早已远去。互联网的发展日新月异,我们仍需在开放中保持警惕。或许,升级到Go 1.8是个好主意?

评论 1

  1. #1

    Thanks, great article.

    Bablofil7年前 (2017-01-08)回复