diff --git a/gumble/gumble/client.go b/gumble/gumble/client.go index 98a6e92..f7585f9 100644 --- a/gumble/gumble/client.go +++ b/gumble/gumble/client.go @@ -3,6 +3,7 @@ package gumble import ( "crypto/tls" "errors" + "fmt" "math" "net" "runtime" @@ -101,6 +102,19 @@ func Dial(config *Config) (*Client, error) { return DialWithDialer(new(net.Dialer), config, nil) } +// tlsServerName returns the hostname portion of a Mumble server address for +// TLS certificate verification and SNI. +func tlsServerName(address string) (string, error) { + host, _, err := net.SplitHostPort(address) + if err != nil { + return "", fmt.Errorf("gumble: derive TLS server name from %q: %w", address, err) + } + if host == "" { + return "", fmt.Errorf("gumble: derive TLS server name from %q: empty host", address) + } + return host, nil +} + // DialWithDialer connects to the Mumble server at the address given in config. // // The function returns after the connection has been established, the initial @@ -120,6 +134,23 @@ func DialWithDialer(dialer *net.Dialer, config *Config, tlsConfig *tls.Config) ( if err != nil { return nil, err } + + // tls.Client cannot infer a server name from an already-open connection. + // Clone the caller's configuration before deriving it so reconnects and + // concurrent clients do not mutate a shared configuration. + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } else { + tlsConfig = tlsConfig.Clone() + } + if tlsConfig.ServerName == "" { + serverName, err := tlsServerName(config.Address) + if err != nil { + rawConn.Close() + return nil, err + } + tlsConfig.ServerName = serverName + } conn := tls.Client(rawConn, tlsConfig) // net.Dialer.Timeout covers only the TCP dial. Apply the same bounded // deadline to TLS negotiation so a peer that accepts but never responds diff --git a/gumble/gumble/client_tls_test.go b/gumble/gumble/client_tls_test.go new file mode 100644 index 0000000..3c2879a --- /dev/null +++ b/gumble/gumble/client_tls_test.go @@ -0,0 +1,28 @@ +package gumble + +import "testing" + +func TestTLSServerNameUsesAddressHost(t *testing.T) { + for _, test := range []struct { + address string + want string + }{ + {"mumble.example:64738", "mumble.example"}, + {"[2001:db8::1]:64738", "2001:db8::1"}, + } { + got, err := tlsServerName(test.address) + if err != nil { + t.Errorf("tlsServerName(%q): %v", test.address, err) + continue + } + if got != test.want { + t.Errorf("tlsServerName(%q) = %q, want %q", test.address, got, test.want) + } + } +} + +func TestTLSServerNameRejectsAddressWithoutHost(t *testing.T) { + if _, err := tlsServerName(":64738"); err == nil { + t.Fatal("tlsServerName accepted an empty host") + } +}