Skip to content

Commit

Permalink
feat: add support for unix sockets (linux, mac, and windows) (#1182)
Browse files Browse the repository at this point in the history
  • Loading branch information
enocom committed Jun 17, 2022
1 parent b686477 commit b2f9d51
Show file tree
Hide file tree
Showing 12 changed files with 462 additions and 50 deletions.
42 changes: 36 additions & 6 deletions cmd/root.go
Expand Up @@ -120,6 +120,8 @@ any client SSL certificates.`,
"Address on which to bind Cloud SQL instance listeners.")
cmd.PersistentFlags().IntVarP(&c.conf.Port, "port", "p", 0,
"Initial port to use for listeners. Subsequent listeners increment from this value.")
cmd.PersistentFlags().StringVarP(&c.conf.UnixSocket, "unix-socket", "u", "",
`Enables Unix sockets for all listeners using the provided directory.`)

c.Command = cmd
return c
Expand All @@ -130,20 +132,29 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
if len(args) == 0 {
return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)")
}
// First, validate global config.

userHasSet := func(f string) bool {
return cmd.PersistentFlags().Lookup(f).Changed
}
if userHasSet("address") && userHasSet("unix-socket") {
return newBadCommandError("cannot specify --unix-socket and --address together")
}
if userHasSet("port") && userHasSet("unix-socket") {
return newBadCommandError("cannot specify --unix-socket and --port together")
}
if ip := net.ParseIP(conf.Addr); ip == nil {
return newBadCommandError(fmt.Sprintf("not a valid IP address: %q", conf.Addr))
}

// If more than one auth method is set, error.
if conf.Token != "" && conf.CredentialsFile != "" {
return newBadCommandError("Cannot specify --token and --credentials-file flags at the same time")
return newBadCommandError("cannot specify --token and --credentials-file flags at the same time")
}
if conf.Token != "" && conf.GcloudAuth {
return newBadCommandError("Cannot specify --token and --gcloud-auth flags at the same time")
return newBadCommandError("cannot specify --token and --gcloud-auth flags at the same time")
}
if conf.CredentialsFile != "" && conf.GcloudAuth {
return newBadCommandError("Cannot specify --credentials-file and --gcloud-auth flags at the same time")
return newBadCommandError("cannot specify --credentials-file and --gcloud-auth flags at the same time")
}
opts := []cloudsqlconn.Option{
cloudsqlconn.WithUserAgent(userAgent),
Expand Down Expand Up @@ -185,7 +196,18 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
return newBadCommandError(fmt.Sprintf("could not parse query: %q", res[1]))
}

if a, ok := q["address"]; ok {
a, aok := q["address"]
p, pok := q["port"]
u, uok := q["unix-socket"]

if aok && uok {
return newBadCommandError("cannot specify both address and unix-socket query params")
}
if pok && uok {
return newBadCommandError("cannot specify both port and unix-socket query params")
}

if aok {
if len(a) != 1 {
return newBadCommandError(fmt.Sprintf("address query param should be only one value: %q", a))
}
Expand All @@ -198,7 +220,7 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
ic.Addr = a[0]
}

if p, ok := q["port"]; ok {
if pok {
if len(p) != 1 {
return newBadCommandError(fmt.Sprintf("port query param should be only one value: %q", a))
}
Expand All @@ -211,6 +233,14 @@ func parseConfig(cmd *cobra.Command, conf *proxy.Config, args []string) error {
}
ic.Port = pp
}

if uok {
if len(u) != 1 {
return newBadCommandError(fmt.Sprintf("unix query param should be only one value: %q", a))
}
ic.UnixSocket = u[0]

}
}
ics = append(ics, ic)
}
Expand Down
43 changes: 43 additions & 0 deletions cmd/root_test.go
Expand Up @@ -151,6 +151,29 @@ func TestNewCommandArguments(t *testing.T) {
GcloudAuth: true,
}),
},
{
desc: "using the unix socket flag",
args: []string{"--unix-socket", "/path/to/dir/", "proj:region:inst"},
want: withDefaults(&proxy.Config{
UnixSocket: "/path/to/dir/",
}),
},
{
desc: "using the (short) unix socket flag",
args: []string{"-u", "/path/to/dir/", "proj:region:inst"},
want: withDefaults(&proxy.Config{
UnixSocket: "/path/to/dir/",
}),
},
{
desc: "using the unix socket query param",
args: []string{"proj:region:inst?unix-socket=/path/to/dir/"},
want: withDefaults(&proxy.Config{
Instances: []proxy.InstanceConnConfig{{
UnixSocket: "/path/to/dir/",
}},
}),
},
}

for _, tc := range tcs {
Expand Down Expand Up @@ -237,6 +260,26 @@ func TestNewCommandWithErrors(t *testing.T) {
"--gcloud-auth",
"--credential-file", "/path/to/file", "proj:region:inst"},
},
{
desc: "when the unix socket query param contains multiple values",
args: []string{"proj:region:inst?unix-socket=/one&unix-socket=/two"},
},
{
desc: "using the unix socket flag with addr",
args: []string{"-u", "/path/to/dir/", "-a", "127.0.0.1", "proj:region:inst"},
},
{
desc: "using the unix socket flag with port",
args: []string{"-u", "/path/to/dir/", "-p", "5432", "proj:region:inst"},
},
{
desc: "using the unix socket and addr query params",
args: []string{"proj:region:inst?unix-socket=/path&address=127.0.0.1"},
},
{
desc: "using the unix socket and port query params",
args: []string{"proj:region:inst?unix-socket=/path&port=5000"},
},
}

for _, tc := range tcs {
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Expand Up @@ -9,12 +9,13 @@ require (
github.com/coreos/go-systemd/v22 v22.3.2
github.com/denisenkom/go-mssqldb v0.12.2
github.com/go-sql-driver/mysql v1.6.0
github.com/google/go-cmp v0.5.7
github.com/google/go-cmp v0.5.8
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/jackc/pgx/v4 v4.16.1
github.com/lib/pq v1.10.6
github.com/spf13/cobra v1.2.1
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d
Expand Down
4 changes: 3 additions & 1 deletion go.sum
Expand Up @@ -443,8 +443,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8=
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -520,6 +521,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
Expand Down
94 changes: 78 additions & 16 deletions internal/proxy/proxy.go
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"
"io"
"net"
"os"
"strings"
"sync"
"time"
Expand All @@ -37,6 +38,10 @@ type InstanceConnConfig struct {
Addr string
// Port is the port on which to bind a listener for the instance.
Port int
// UnixSocket is the directory where a Unix socket will be created,
// connected to the Cloud SQL instance. If set, takes precedence over Addr
// and Port.
UnixSocket string
}

// Config contains all the configuration provided by the caller.
Expand All @@ -58,6 +63,10 @@ type Config struct {
// increments from this value.
Port int

// UnixSocket is the directory where Unix sockets will be created,
// connected to any Instances. If set, takes precedence over Addr and Port.
UnixSocket string

// Instances are configuration for individual instances. Instance
// configuration takes precedence over global configuration.
Instances []InstanceConnConfig
Expand Down Expand Up @@ -134,31 +143,84 @@ func NewClient(ctx context.Context, d cloudsql.Dialer, cmd *cobra.Command, conf
}
pc := newPortConfig(conf.Port)
for _, inst := range conf.Instances {
m := &socketMount{inst: inst.Name}
a := conf.Addr
if inst.Addr != "" {
a = inst.Addr
}
version, err := d.EngineVersion(ctx, inst.Name)
if err != nil {
return nil, err
}
var np int
switch {
case inst.Port != 0:
np = inst.Port
case conf.Port != 0:
np = pc.nextPort()
default:
np = pc.nextDBPort(version)

var (
// network is one of "tcp" or "unix"
network string
// address is either a TCP host port, or a Unix socket
address string
)
// IF
// a global Unix socket directory is NOT set AND
// an instance-level Unix socket is NOT set
// (e.g., I didn't set a Unix socket globally or for this instance)
// OR
// an instance-level TCP address or port IS set
// (e.g., I'm overriding any global settings to use TCP for this
// instance)
// use a TCP listener.
// Otherwise, use a Unix socket.
if (conf.UnixSocket == "" && inst.UnixSocket == "") ||
(inst.Addr != "" || inst.Port != 0) {
network = "tcp"

a := conf.Addr
if inst.Addr != "" {
a = inst.Addr
}

var np int
switch {
case inst.Port != 0:
np = inst.Port
case conf.Port != 0:
np = pc.nextPort()
default:
np = pc.nextDBPort(version)
}

address = net.JoinHostPort(a, fmt.Sprint(np))
} else {
network = "unix"

dir := conf.UnixSocket
if dir == "" {
dir = inst.UnixSocket
}
// Attempt to make the directory if it does not already exist.
if _, err := os.Stat(dir); err != nil {
if err = os.Mkdir(dir, 0770); err != nil {
return nil, err
}
}
address = UnixAddress(dir, inst.Name)
// When setting up a listener for Postgres, create address as a
// directory, and use the Postgres-specific socket name
// .s.PGSQL.5432.
if strings.HasPrefix(version, "POSTGRES") {
// Make the directory only if it hasn't already been created.
if _, err := os.Stat(address); err != nil {
if err = os.Mkdir(address, 0770); err != nil {
return nil, err
}
}
address = UnixAddress(address, ".s.PGSQL.5432")
}
}
addr, err := m.listen(ctx, "tcp", net.JoinHostPort(a, fmt.Sprint(np)))

m := &socketMount{inst: inst.Name}
addr, err := m.listen(ctx, network, address)
if err != nil {
for _, m := range mnts {
m.close()
}
return nil, fmt.Errorf("[%v] Unable to mount socket: %v", inst.Name, err)
}

cmd.Printf("[%s] Listening on %s\n", inst.Name, addr.String())
mnts = append(mnts, m)
}
Expand Down Expand Up @@ -241,9 +303,9 @@ type socketMount struct {
}

// listen causes a socketMount to create a Listener at the specified network address.
func (s *socketMount) listen(ctx context.Context, network string, host string) (net.Addr, error) {
func (s *socketMount) listen(ctx context.Context, network string, address string) (net.Addr, error) {
lc := net.ListenConfig{KeepAlive: 30 * time.Second}
l, err := lc.Listen(ctx, network, host)
l, err := lc.Listen(ctx, network, address)
if err != nil {
return nil, err
}
Expand Down
27 changes: 27 additions & 0 deletions internal/proxy/proxy_other.go
@@ -0,0 +1,27 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !windows
// +build !windows

package proxy

import "path/filepath"

// UnixAddress is defined as a function to distinguish between Linux-based
// implementations where the dir and inst and simply joins, and Windows-based
// implementations where the inst must be further altered.
func UnixAddress(dir, inst string) string {
return filepath.Join(dir, inst)
}
27 changes: 27 additions & 0 deletions internal/proxy/proxy_other_test.go
@@ -0,0 +1,27 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build !windows
// +build !windows

package proxy_test

var (
pg = "proj:region:pg"
pg2 = "proj:region:pg2"
mysql = "proj:region:mysql"
mysql2 = "proj:region:mysql2"
sqlserver = "proj:region:sqlserver"
sqlserver2 = "proj:region:sqlserver2"
)

0 comments on commit b2f9d51

Please sign in to comment.