After getting a basic idea of what TLS is in the previous post, let’s write a tool the will help us finding out the TLS version of a server.
NOTE: You can read this post also on github.
The first idea might be to range over the arguments that should be TCP addresses. For each address we’ll get and print the TLS version. We might start with a pseudo-code like:
for all IP addresses supplied as CLI arguments:
get the the TLS version
This look simple enough to implement:
package main
import (
"crypto/tls"
"fmt"
"os"
)
func main() {
for _, addr := range os.Args[1:] {
, err := getTLSVersion(addr)
verif err != nil {
.Fprintf(os.Stderr, "tlsver: %v", err)
fmtcontinue
}
.Printf("%s\t%s\n", ver, addr)
fmt}
}
Now we need to write the GetTLSVersion function. Fortunately there’s the standard library package tls:
func getTLSVersion(addr string) (string, error) {
, err := tls.Dial("tcp", addr, &tls.Config{InsecureSkipVerify: true})
connif err != nil {
return "", err
}
defer conn.Close()
return tls.VersionName(conn.ConnectionState().Version), nil
}
Let’s run the program (saved under filename
tlsver.go
):
$ go run tlsver/1/tlsver.go example.com:443 example.net:443 wall.org:443
TLS 1.3 example.com:443
TLS 1.3 example.net:443
TLS 1.2 wall.org:443
As you can see, Larry is a bit behind :-).
See the whole program at https://github.com/jreisinger/docs/blog/gosec/tlsver/1.
Adding concurrency
The code above works fine. But imagine that we want to check the TLS version of a thousand hosts. Connecting to the hosts one after another might take some time. Concurrency means organizing the program in a way that multiple processes can execute independently. Even at the same time if you have multiple processors (which you most certainly do nowadays). Go has an excellent support for doing this.
Basically we want to run the getTLSVersion function and forget about.
Then run the next one and forget about it (like when you background a
shell command with &
). And so on. Obviously we need to
feed some input (function parameters) into the function and collect its
output (return values). We use in
and out
channels for this. The channels are like typed shell pipes
($ ls | wc -l
). The type in our case is host
-
it holds all the necessary information. When we fire up goroutines we
usually don’t want to allow for an unlimited number of them because we
might exhaust computing resources (like open sockets or file
descriptors). So we run only 30 goroutines. We also want to know when
the goroutines are done. For this we use the WaitGroup
,
which is kind of a concurrency-safe counter.
type host struct {
string
addr string
tlsVer bool
insecure error
err }
:= make(chan host)
in := make(chan host)
out var wg sync.WaitGroup
for i := 0; i < 30; i++ {
.Add(1)
wggo func() {
for h := range in {
.tlsVer, h.err = getTLSVersion(h.addr, h.insecure)
h<- h
out }
.Done()
wg}()
}
We want to get the list of TCP addresses from command line arguments
or from standard input. We don’t want to wait (block) on the input so we
run it in a goroutine as well. When there’s no more input we close the
in
channel and decrease the WaitGroup
counter.
go func() {
if len(flag.Args()) > 0 {
for _, addr := range flag.Args() {
<- host{addr: addr, insecure: *insecure}
in }
} else {
:= bufio.NewScanner(os.Stdin)
s for s.Scan() {
<- host{addr: s.Text(), insecure: *insecure}
in }
}
close(in)
.Done()
wg}()
And we just print the output coming from the out
channel. We close the channel when the WaitGroup
counter is
zero, i.e. all goroutines are done.
go func() {
.Wait()
wgclose(out)
}()
for h := range out {
if h.err != nil {
.Fprintf(os.Stderr, "%s: %v", h.addr, h.err)
fmt}
.Printf("%s\t%s\n", h.tlsVer, h.addr)
fmt}
Now we can quickly check many hosts:
$ go install tlsver/2/tlsver.go
$ cat ~/Downloads/top1000domains.txt | tlsver -insecure -concurrency 10
TLS 1.3 facebook.com:443
TLS 1.3 youtube.com:443
TLS 1.2 bing.com:443
<...SNIP...>
See the whole program at https://github.com/jreisinger/docs/blog/gosec/tlsver/2.
Tips for designing programs
Design iteratively. No one designs a program top to bottom in a linear, systematic fashion.
Try out alternatives. Good design involves a lot of trial and error. When you look at someone’s code, it’s finished work, not the process they went through to get there.
Keep it simple. Don’t design in extra complexity until it is really needed.
Solve one problem at a time, don’t be overwhelmed by everything.