Go socket servers with TLS

TLS (previously known as SSL) is best known for enabling HTTPS, a secure version
of HTTP. However, as TLS’s name (Transport Layer Security) suggests, it
actually goes deeper than HTTP. TLS is best thought of as a secure version of
TCP; in other words, it provides an ability to encrypt and sign arbitrary
communications going over sockets [1]. For example, protocols like gRPC build
on top of TLS for security.

In an earlier post we’ve seen how
to use TLS to set up HTTPS servers and clients in Go. Here, I want to show how
to create an encrypted socket server that could serve as a basis for other
network protocols. All the code for this post is available on GitHub.

TLS socket server

Here’s a basic echo server using TLS:

func main() {
port := flag.String(“port”, “4040”, “listening port”)
certFile := flag.String(“cert”, “cert.pem”, “certificate PEM file”)
keyFile := flag.String(“key”, “key.pem”, “key PEM file”)

cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
if err != nil {
config := &tls.Config{Certificates: []tls.Certificate{cert}}

log.Printf(“listening on port %sn”, *port)
l, err := tls.Listen(“tcp”, “:”+*port, config)
if err != nil {
defer l.Close()

for {
conn, err := l.Accept()
if err != nil {
log.Printf(“accepted connection from %sn”, conn.RemoteAddr())

go func(c net.Conn) {
io.Copy(c, c)
log.Printf(“closing connection from %sn”, conn.RemoteAddr())

It accepts multiple (concurrent) connections from clients, and mirrors back all
data the clients send, until the client’s socket is closed. It’s very similar
to how a non-TLS server would look, except that net.Listen is replaced with
tls.Listen, and the latter needs a tls.Config (which we’ve already
encountered in the previous post). We can use
the certificate generating tool shown in that post, or the mkcert tool to
generate a certificate/key pair for this server.

TLS socket client

And now the companion client:

func main() {
port := flag.String(“port”, “4040”, “port to connect”)
certFile := flag.String(“certfile”, “cert.pem”, “trusted CA certificate”)

cert, err := os.ReadFile(*certFile)
if err != nil {
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(cert); !ok {
log.Fatalf(“unable to parse cert from %s”, *certFile)
config := &tls.Config{RootCAs: certPool}

conn, err := tls.Dial(“tcp”, “localhost:”+*port, config)
if err != nil {

_, err = io.WriteString(conn, “Hello simple secure Servern”)
if err != nil {
log.Fatal(“client write error:”, err)
if err = conn.CloseWrite(); err != nil {

buf := make([]byte, 256)
n, err := conn.Read(buf)
if err != nil && err != io.EOF {

fmt.Println(“client read:”, string(buf[:n]))

Again, the difference from a non-TLS client is just replacing net.Dial by
tls.Dial and the accompanying tls.Config filled in with a certificate
the client can trust (this can be either the server’s own certificate, or the
certificate of the CA signing the server’s certificate, and so on).

Examining server certificate chains

Here’s a simple program (tls-dial-port) we can use to examine the
certificate chain of any server:

func main() {
addr := flag.String(“addr”, “localhost:4040”, “dial address”)

cfg := tls.Config{}
conn, err := tls.Dial(“tcp”, *addr, &cfg)
if err != nil {
log.Fatal(“TLS connection failed: “ + err.Error())
defer conn.Close()

certChain := conn.ConnectionState().PeerCertificates
for i, cert := range certChain {
fmt.Println(“Issuer:”, cert.Issuer)
fmt.Println(“Subject:”, cert.Subject)
fmt.Println(“Version:”, cert.Version)
fmt.Println(“NotAfter:”, cert.NotAfter)
fmt.Println(“DNS names:”, cert.DNSNames)

Given an IP address, this program opens up a TLS connection to the server and
reports the certificates it uses. We can try it with our own TLS socket server
first; note that this program doesn’t have any pre-trusted certificates beyond
the system defaults, so it will reject self-signed certificates. However, if
we use mkcert to generate a certificate for our server, it will work.

In one terminal, let’s run our TLS socket server with a mkcert-generated

$ mkcert localhost

Created a new certificate valid for the following names 📜
– “localhost”

The certificate is at “./localhost.pem” and the key at “./localhost-key.pem” ✅

It will expire on 7 July 2023 🗓

$ go run tls-socket-server.go -cert localhost.pem -key localhost-key.pem
2021/04/07 06:27:20 listening on port 4040

In a separate terminal, run tls-dial-port:

$ go run tls-dial-port.go -addr localhost:4040
Issuer: CN=mkcert [email protected] (Eli Bendersky),[email protected] (Eli Bendersky),O=mkcert development CA
Subject: [email protected] (Eli Bendersky),O=mkcert development certificate
Version: 3
NotAfter: 2023-07-07 13:27:12 +0000 UTC
DNS names: [localhost]

We see the certificate mkcert generated. Since mkcert added this
certificate to my machine’s system root store, it will be trusted by the default
settings of tls.Dial.

We can also point this program at port 443 (the default port for HTTPS) of
public websites; for example:

$ go run tls-dial-port.go -addr reddit.com:443
Issuer: CN=DigiCert TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US
Subject: CN=*.reddit.com,O=Reddit Inc.,L=San Francisco,ST=California,C=US
Version: 3
NotAfter: 2021-07-06 23:59:59 +0000 UTC
DNS names: [reddit.com *.reddit.com]

Issuer: CN=DigiCert Global Root CA,OU=www.digicert.com,O=DigiCert Inc,C=US
Subject: CN=DigiCert TLS RSA SHA256 2020 CA1,O=DigiCert Inc,C=US
Version: 3
NotAfter: 2030-09-23 23:59:59 +0000 UTC
DNS names: []

Prior to the TLS standards, an older version of the protocol was called
SSL – Secure Socket Layer, which is even more suggestive of this use.

Note that here I show the machinery for a client to trust the server,
not the other way around. In the HTTPS post we’ve
also seen how to set up mTLS for mutual authentication, and the same
technique can easily be applied here.

Flatlogic Admin Templates banner

Leave a Reply

Your email address will not be published. Required fields are marked *