├── version.txt ├── asset ├── 01.PNG ├── 02.PNG ├── 03.PNG └── manual.png ├── .github ├── blunderbuss.yml ├── renovate.json ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── question.md │ ├── documentation-issue.md │ ├── feature-request.md │ └── bug-report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── update-versions.yml │ ├── release-please.yaml │ └── tests.yaml ├── .gitignore ├── examples └── kubernetes │ ├── service_account.yaml │ ├── no_proxy_private_ip.yaml │ ├── proxy_with_workload_identity.yaml │ ├── proxy_with_sa_key.yaml │ └── README.md ├── .kokoro ├── trampoline.sh ├── go113 │ └── linux │ │ ├── periodic.cfg │ │ ├── presubmit.cfg │ │ ├── continuous.cfg │ │ └── common.cfg ├── go114 │ ├── linux │ │ ├── periodic.cfg │ │ ├── presubmit.cfg │ │ ├── continuous.cfg │ │ └── common.cfg │ └── macos │ │ ├── periodic.cfg │ │ ├── presubmit.cfg │ │ ├── continuous.cfg │ │ └── common.cfg ├── go115 │ └── linux │ │ ├── periodic.cfg │ │ ├── presubmit.cfg │ │ ├── continuous.cfg │ │ └── common.cfg ├── tests │ ├── run_tests.sh │ └── run_tests_macos.sh └── release_artifacts.sh ├── README.zh.md ├── go.mod ├── proxy ├── limits │ ├── limits_windows.go │ ├── limits.go │ ├── limits_freebsd.go │ └── limits_test.go ├── fuse │ ├── fuse_windows.go │ ├── fuse_openbsd.go │ ├── fuse_test.go │ └── fuse.go ├── README.md ├── dialers │ ├── postgres │ │ ├── hook_test.go │ │ └── hook.go │ └── mysql │ │ ├── hook_test.go │ │ └── hook.go ├── util │ ├── cloudsqlutil_test.go │ ├── cloudsqlutil.go │ └── gcloudutil.go ├── proxy │ ├── common_test.go │ ├── dial.go │ ├── common.go │ └── client_test.go └── certs │ └── certs.go ├── Dockerfile ├── .build ├── default.yaml ├── alpine.yaml ├── buster.yaml └── gcs_upload.yaml ├── Dockerfile.alpine ├── Dockerfile.buster ├── CONTRIBUTORS ├── logging └── logging.go ├── README.md ├── tests ├── sqlserver_test.go ├── postgres_test.go ├── mysql_test.go ├── connection_test.go └── common_test.go ├── CHANGELOG.md ├── CONTRIBUTING.md ├── cmd └── cloud_sql_proxy │ ├── proxy_test.go │ └── proxy.go └── LICENSE /version.txt: -------------------------------------------------------------------------------- 1 | 1.19.0 2 | -------------------------------------------------------------------------------- /asset/01.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Funny-Systems-OSS/cloudsql-proxy-hardening/HEAD/asset/01.PNG -------------------------------------------------------------------------------- /asset/02.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Funny-Systems-OSS/cloudsql-proxy-hardening/HEAD/asset/02.PNG -------------------------------------------------------------------------------- /asset/03.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Funny-Systems-OSS/cloudsql-proxy-hardening/HEAD/asset/03.PNG -------------------------------------------------------------------------------- /asset/manual.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Funny-Systems-OSS/cloudsql-proxy-hardening/HEAD/asset/manual.png -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | assign_issues: 2 | - shubha-rajan 3 | # - kurtisvg 4 | 5 | assign_prs: 6 | - shubha-rajan 7 | # - kurtisvg 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # direnv 2 | .envrc 3 | 4 | # IDEs 5 | .idea/ 6 | .vscode/ 7 | 8 | # Compiled binary 9 | cmd/cloud_sql_proxy/cloud_sql_proxy 10 | -------------------------------------------------------------------------------- /examples/kubernetes/service_account.yaml: -------------------------------------------------------------------------------- 1 | # [START cloud_sql_proxy_k8s_sa_yml] 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: # TODO(developer): replace these values 6 | # [END cloud_sql_proxy_k8s_sa_yml] -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "prConcurrentLimit": 0, 6 | "rebaseStalePrs": true, 7 | "masterIssue": true, 8 | "semanticCommits": true, 9 | "postUpdateOptions": [ 10 | "gomodTidy" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Cloud SQL Issue tracker 4 | url: https://issuetracker.google.com/savedsearches/559773 5 | about: Please use the Cloud SQL Issue tracker for problems with Cloud SQL itself. 6 | - name: StackOverflow 7 | url: https://stackoverflow.com/questions/tagged/google-cloud-sql 8 | about: Please use the `google-cloud-sql` tag for questions on StackOverflow. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Change Description 2 | 3 | Please provide a detailed description on what changes your PR will have. 4 | 5 | 6 | ## Checklist 7 | 8 | - [ ] Make sure to open an issue as a 9 | [bug/issue](https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/new/choose) 10 | before writing your code! That way we can discuss the change, evaluate 11 | designs, and agree on the general idea. 12 | - [ ] Ensure the tests and linter pass 13 | - [ ] Appropriate documentation is updated (if necessary) 14 | 15 | ## Relevant issues: 16 | 17 | - Fixes # -------------------------------------------------------------------------------- /.kokoro/trampoline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2019 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | python3 "${KOKORO_GFILE_DIR}/trampoline_v1.py" -------------------------------------------------------------------------------- /.kokoro/go113/linux/periodic.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go113/linux/presubmit.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go114/linux/periodic.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go114/linux/presubmit.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go114/macos/periodic.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go114/macos/presubmit.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go115/linux/periodic.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go115/linux/presubmit.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go113/linux/continuous.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go114/linux/continuous.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go114/macos/continuous.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /.kokoro/go115/linux/continuous.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | 2 | # Cloud SQL Proxy Hardening 3 | 4 | + [功能](#功能) 5 | + [需求](#需求) 6 | + [安裝](#安裝) 7 | + [使用](#使用) 8 | + [兔肚](#兔肚) 9 | + [簡報](#簡報) 10 | ## Fork from 11 | cloudsql-proxy: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/v1.19.0 12 | ## 功能 13 | + 將明文金鑰以綁定 GCE 執行個體 ID 的加密檔案取代。 14 | ## 需求 15 | + Go 1.15 以上。 16 | ## 安裝 17 | 1. git clone https://github.com/Funny-Systems-OSS/cloudsql-proxy-hardening.git 18 | 2. cd ./cloudsql-proxy-hardening 19 | 3. go build -o ../cloud_sql_proxy_funny ./cmd/cloud_sql_proxy/ 20 | ## 使用 21 | + ./cloud_sql_proxy_funny <-credential_file 金鑰路徑> [-use_plainfile] 22 | + -credential_file:\ 23 | 用於在 cloud_sql_proxy_funny 中認證服務帳號的金鑰。 24 | + -use_plainfile:\ 25 | 可選參數。設置時可使用明文金鑰檔。 26 | ## 兔肚 27 | + 非 debug 模式假裝自己是一般 cloud_sql_proxy 28 | ## [簡報](https://speakerdeck.com/funnysystems/wan-gu-ba-gcp-cloud-sql-why-hardening-gcp-cloud-sql) 29 | -------------------------------------------------------------------------------- /.kokoro/go114/macos/common.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | 17 | # Get secrets for tests. 18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" 19 | 20 | # Use the trampoline script to run in docker. 21 | build_file: "cloud-sql-proxy/.kokoro/tests/run_tests_macos.sh" 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/GoogleCloudPlatform/cloudsql-proxy 2 | 3 | go 1.13 4 | 5 | require ( 6 | bazil.org/fuse v0.0.0-20200524192727-fb710f7dfd05 7 | cloud.google.com/go v0.73.0 8 | github.com/Funny-Systems-OSS/CloudSQL-Proxy-Hardening-Common v0.0.0-20201209081344-189bc3523013 9 | github.com/denisenkom/go-mssqldb v0.9.0 10 | github.com/go-sql-driver/mysql v1.5.0 11 | github.com/lib/pq v1.9.0 12 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c // indirect 13 | golang.org/x/net v0.0.0-20201207224615-747e23833adb 14 | golang.org/x/oauth2 v0.0.0-20201207163604-931764155e3f 15 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d // indirect 16 | google.golang.org/api v0.36.0 17 | google.golang.org/genproto v0.0.0-20201207150747-9ee31aac76e7 // indirect 18 | google.golang.org/grpc v1.34.0 // indirect 19 | ) 20 | 21 | replace bazil.org/fuse => bazil.org/fuse v0.0.0-20180421153158-65cc252bf669 // pin to latest version that supports macOS. see https://github.com/bazil/fuse/issues/224 22 | -------------------------------------------------------------------------------- /.github/workflows/update-versions.yml: -------------------------------------------------------------------------------- 1 | 2 | name: update-version 3 | on: 4 | pull_request: 5 | branches: ['master'] 6 | paths: 7 | - 'version.txt' 8 | 9 | jobs: 10 | update-version-in-pr: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | ref: ${{ github.head_ref }} 17 | 18 | - if: github.actor == 'github-actions[bot]' 19 | name: find-and-replace-version 20 | run: | 21 | VERSION=$(cat version.txt) 22 | sed -i -r 's/(var versionString = ")[0-9]+.[0-9]+.[0-9]+(-dev)?/\1'"$VERSION"'/' cmd/cloud_sql_proxy/cloud_sql_proxy.go 23 | git config user.name "github-actions[bot]" 24 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 25 | git add cmd/cloud_sql_proxy/cloud_sql_proxy.go 26 | git commit -m "Update version" 27 | git push origin ${{ github.head_ref }} 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: release-please 16 | on: 17 | push: 18 | branches: 19 | - master 20 | jobs: 21 | release-please: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: GoogleCloudPlatform/release-please-action@v2.6.0 25 | with: 26 | token: ${{ secrets.GITHUB_TOKEN }} 27 | release-type: simple 28 | package-name: cloud-sql-proxy 29 | 30 | -------------------------------------------------------------------------------- /proxy/limits/limits_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package limits is a package stub for windows, and we currently don't support 16 | // setting limits in windows. 17 | package limits 18 | 19 | import "errors" 20 | 21 | // We don't support limit on the number of file handles in windows. 22 | const ExpectedFDs = 0 23 | 24 | func SetupFDLimits(wantFDs uint64) error { 25 | if wantFDs != 0 { 26 | return errors.New("setting limits on the number of file handles is not supported") 27 | } 28 | 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use the latest stable golang 1.x to compile to a binary 16 | FROM golang:1 as build 17 | 18 | WORKDIR /go/src/cloudsql-proxy 19 | COPY . . 20 | 21 | RUN go get ./... 22 | RUN go build -ldflags "-X main.metadataString=container" -o cloud_sql_proxy ./cmd/cloud_sql_proxy 23 | 24 | # Final Stage 25 | FROM gcr.io/distroless/base-debian10:nonroot 26 | COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy 27 | # set the uid as an integer for compatibility with runAsNonRoot in Kubernetes 28 | USER 65532 29 | -------------------------------------------------------------------------------- /proxy/fuse/fuse_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fuse is a package stub for windows, which does not support FUSE. 16 | package fuse 17 | 18 | import ( 19 | "errors" 20 | "io" 21 | 22 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" 23 | ) 24 | 25 | func Supported() bool { 26 | return false 27 | } 28 | 29 | func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { 30 | return nil, nil, errors.New("fuse not supported on windows") 31 | } 32 | -------------------------------------------------------------------------------- /proxy/fuse/fuse_openbsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package fuse is a package stub for openbsd, which isn't supported by our 16 | // fuse library. 17 | package fuse 18 | 19 | import ( 20 | "errors" 21 | "io" 22 | 23 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" 24 | ) 25 | 26 | func Supported() bool { 27 | return false 28 | } 29 | 30 | func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { 31 | return nil, nil, errors.New("fuse not supported on openbsd") 32 | } 33 | -------------------------------------------------------------------------------- /examples/kubernetes/no_proxy_private_ip.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: 9 | template: 10 | metadata: 11 | labels: 12 | app: 13 | spec: 14 | containers: 15 | - name: 16 | # ... other container configuration 17 | env: 18 | - name: DB_USER 19 | valueFrom: 20 | secretKeyRef: 21 | name: 22 | key: username 23 | - name: DB_PASS 24 | valueFrom: 25 | secretKeyRef: 26 | name: 27 | key: password 28 | - name: DB_NAME 29 | valueFrom: 30 | secretKeyRef: 31 | name: 32 | key: database 33 | # [START cloud_sql_proxy_secret_host] 34 | - name: DB_HOST 35 | valueFrom: 36 | secretKeyRef: 37 | name: 38 | key: db_host 39 | # [END cloud_sql_proxy_secret_host] 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Questions on how something works or the best way to do something. 4 | title: "Breif summary of your question" 5 | labels: 'type: question' 6 | 7 | --- 8 | 9 | 25 | 26 | ## Question 27 | What's your question? Please provide as much relevant information as possible 28 | to reduce turnaround time. 29 | 30 | ## Additional Context 31 | Please reference any other relevant issues, PRs, descriptions, or screenshots here. 32 | -------------------------------------------------------------------------------- /.build/default.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: 18 | - 'build' 19 | - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 20 | - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 21 | - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 22 | - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 23 | - '.' 24 | images: 25 | - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 26 | - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 27 | - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' 28 | - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}' -------------------------------------------------------------------------------- /proxy/README.md: -------------------------------------------------------------------------------- 1 | # Cloud SQL proxy dialer for Go 2 | 3 | You can also use the Cloud SQL proxy directly from a Go program. 4 | 5 | These packages are primarily used as implementation for the Cloud SQL proxy command, 6 | and may be changed in backwards incompatible ways in the future. 7 | 8 | ## To use inside a Go program: 9 | If your program is written in [Go](https://golang.org) you can use the Cloud SQL Proxy as a library, 10 | avoiding the need to start the Proxy as a companion process. 11 | 12 | ### MySQL 13 | If you're using the the MySQL [go-sql-driver](https://github.com/go-sql-driver/mysql) 14 | you can use helper functions found in the [`proxy/dialers/mysql` package](https://godoc.org/github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql). See [example usage](https://github.com/GoogleCloudPlatform/cloudsql-proxy/blob/master/tests/dialers_test.go). 15 | 16 | ### Postgres 17 | If you're using the the Postgres [lib/pq](https://github.com/lib/pq), you can use the `cloudsqlpostgres` driver from [here](https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/master/proxy/dialers/postgres). See [example usage](https://github.com/GoogleCloudPlatform/cloudsql-proxy/blob/master/proxy/dialers/postgres/hook_test.go). 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: tests 16 | on: [pull_request] 17 | 18 | jobs: 19 | build: 20 | name: lint 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Setup Go 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: '1.13' 27 | - name: Install goimports 28 | run: go get golang.org/x/tools/cmd/goimports 29 | - name: Checkout code 30 | uses: actions/checkout@v2 31 | - run: goimports -w . 32 | - run: go mod tidy 33 | - name: Verify no changes from goimports and go mod tidy. If you're reading this and the check has failed, run `goimports -w . && go mod tidy`. 34 | run: git diff --exit-code -------------------------------------------------------------------------------- /.build/alpine.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: 18 | - 'build' 19 | - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 20 | - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 21 | - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 22 | - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 23 | - '-f=Dockerfile.alpine' 24 | - '.' 25 | images: 26 | - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 27 | - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 28 | - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' 29 | - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-alpine' -------------------------------------------------------------------------------- /.build/buster.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | steps: 16 | - name: 'gcr.io/cloud-builders/docker' 17 | args: 18 | - 'build' 19 | - '--tag=gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 20 | - '--tag=us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 21 | - '--tag=eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 22 | - '--tag=asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 23 | - '-f=Dockerfile.buster' 24 | - '.' 25 | images: 26 | - 'gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 27 | - 'us.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 28 | - 'eu.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' 29 | - 'asia.gcr.io/$PROJECT_ID/gce-proxy:${_VERSION}-buster' -------------------------------------------------------------------------------- /.kokoro/go113/linux/common.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | 17 | # Get secrets for tests. 18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" 19 | 20 | # Download trampoline resources. 21 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 22 | 23 | # Use the trampoline script to run in docker. 24 | build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" 25 | 26 | env_vars: { 27 | key: "TRAMPOLINE_IMAGE" 28 | value: "golang:1.13" 29 | } 30 | 31 | # Tell the trampoline which tests to run. 32 | env_vars: { 33 | key: "TRAMPOLINE_BUILD_FILE" 34 | value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" 35 | } -------------------------------------------------------------------------------- /.kokoro/go114/linux/common.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | 17 | # Get secrets for tests. 18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" 19 | 20 | # Download trampoline resources. 21 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 22 | 23 | # Use the trampoline script to run in docker. 24 | build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" 25 | 26 | env_vars: { 27 | key: "TRAMPOLINE_IMAGE" 28 | value: "golang:1.14" 29 | } 30 | 31 | # Tell the trampoline which tests to run. 32 | env_vars: { 33 | key: "TRAMPOLINE_BUILD_FILE" 34 | value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" 35 | } -------------------------------------------------------------------------------- /.kokoro/go115/linux/common.cfg: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Format: //devtools/kokoro/config/proto/build.proto 16 | 17 | # Get secrets for tests. 18 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/cloud-sql/proxy" 19 | 20 | # Download trampoline resources. 21 | gfile_resources: "/bigstore/cloud-devrel-kokoro-resources/trampoline" 22 | 23 | # Use the trampoline script to run in docker. 24 | build_file: "cloud-sql-proxy/.kokoro/trampoline.sh" 25 | 26 | env_vars: { 27 | key: "TRAMPOLINE_IMAGE" 28 | value: "golang:1.15" 29 | } 30 | 31 | # Tell the trampoline which tests to run. 32 | env_vars: { 33 | key: "TRAMPOLINE_BUILD_FILE" 34 | value: "github/cloud-sql-proxy/.kokoro/tests/run_tests.sh" 35 | } -------------------------------------------------------------------------------- /.kokoro/tests/run_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2020 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # `-e` enables the script to automatically fail when a command fails 17 | set -e 18 | 19 | export GO111MODULE=on 20 | 21 | # Kokoro setup 22 | if [ -n "$KOKORO_GFILE_DIR" ]; then 23 | # Move into project directory 24 | cd github/cloud-sql-proxy 25 | # install fuse project 26 | apt-get -qq update && apt-get -qq install fuse -y 27 | # source secrets 28 | source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" 29 | export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" 30 | fi 31 | 32 | echo -e "******************** Running tests... ********************\n" 33 | go test -v ./... 34 | echo -e "******************** Tests complete. ********************\n" -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation Issue 3 | about: Report wrong or missing information with the documentation in the repo. 4 | title: "Brief summary of what is missing or incorrect" 5 | labels: 'type: docs' 6 | 7 | --- 8 | 9 | 25 | ## Description 26 | Provide a short description of what is missing or incorrect, as well as a link to the specific location of the information. 27 | 28 | ## Solution 29 | What would you prefer the documentation say? Why would this information be more accurate or helpful? 30 | 31 | ## Additional Context 32 | Please reference any other relevant issues, PRs, descriptions, or screenshots here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for new or improved behavior. 4 | title: "Brief summary of the proposed feature" 5 | labels: 'type: feature request' 6 | 7 | --- 8 | 9 | 25 | ## Feature Description 26 | A clear and concise description of what feature you would like to see, and why it would be useful to have added. 27 | 28 | ## Alternatives Considered 29 | Are there any workaround or third party tools to replicate this behavior? Why would adding this feature be preferred over them? 30 | 31 | ## Additional Context 32 | Please reference any other issues, PRs, descriptions, or screenshots here. 33 | -------------------------------------------------------------------------------- /.kokoro/tests/run_tests_macos.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2020 Google Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDIcd TIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # `-e` enables the script to automatically fail when a command fails 17 | set -e 18 | 19 | export GO111MODULE=on 20 | 21 | # kokoro setup 22 | if [ -n "$KOKORO_GFILE_DIR" ]; then 23 | # move into project directory 24 | cd github/cloud-sql-proxy 25 | # install fuse project 26 | brew update > /dev/null 27 | brew cask install --quiet osxfuse 28 | # source secrets 29 | source "${KOKORO_GFILE_DIR}/TEST_SECRETS.sh" 30 | export GOOGLE_APPLICATION_CREDENTIALS="${KOKORO_GFILE_DIR}/testing-service-account.json" 31 | fi 32 | 33 | # On macOS, the default $TMPDIR is too long for suitable use due to the unix socket length limits 34 | export TMPDIR="/tmp" 35 | echo -e "******************** Running tests... ********************\n" 36 | go test -v ./... 37 | echo -e "******************** Tests complete. ********************\n" -------------------------------------------------------------------------------- /proxy/dialers/postgres/hook_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // Package postgres_test contains an example on how to use cloudsqlpostgres dialer 15 | package postgres_test 16 | 17 | import ( 18 | "database/sql" 19 | "fmt" 20 | "log" 21 | "time" 22 | 23 | _ "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/postgres" 24 | ) 25 | 26 | // Example shows how to use cloudsqlpostgres dialer 27 | func Example() { 28 | // Note that sslmode=disable is required it does not mean that the connection 29 | // is unencrypted. All connections via the proxy are completely encrypted. 30 | db, err := sql.Open("cloudsqlpostgres", "host=project:region:instance user=postgres dbname=postgres password=password sslmode=disable") 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | defer db.Close() 35 | var now time.Time 36 | fmt.Println(db.QueryRow("SELECT NOW()").Scan(&now)) 37 | fmt.Println(now) 38 | } 39 | -------------------------------------------------------------------------------- /Dockerfile.alpine: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use the latest stable golang 1.x to compile to a binary 16 | FROM golang:1 as build 17 | 18 | WORKDIR /go/src/cloudsql-proxy 19 | COPY . . 20 | 21 | RUN go get ./... 22 | RUN go build -ldflags "-X main.metadataString=container.alpine" -o cloud_sql_proxy ./cmd/cloud_sql_proxy 23 | 24 | # Final stage 25 | FROM alpine:3 26 | RUN apk add --no-cache \ 27 | ca-certificates \ 28 | libc6-compat 29 | # Install fuse and allow enable non-root users to mount 30 | RUN apk add --no-cache fuse && sed -i 's/^#user_allow_other$/user_allow_other/g' /etc/fuse.conf 31 | # Add a non-root user matching the nonroot user from the main container 32 | RUN addgroup -g 65532 -S nonroot && adduser -u 65532 -S nonroot -G nonroot 33 | # Set the uid as an integer for compatibility with runAsNonRoot in Kubernetes 34 | USER 65532 35 | 36 | COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy 37 | -------------------------------------------------------------------------------- /Dockerfile.buster: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Use the latest stable golang 1.x to compile to a binary 16 | FROM golang:1 as build 17 | 18 | WORKDIR /go/src/cloudsql-proxy 19 | COPY . . 20 | 21 | RUN go get ./... 22 | RUN go build -ldflags "-X main.metadataString=container.buster" -o cloud_sql_proxy ./cmd/cloud_sql_proxy 23 | 24 | # Final stage 25 | FROM debian:buster 26 | RUN apt-get update && apt-get install -y ca-certificates 27 | # Install fuse and allow enable non-root users to mount 28 | RUN apt-get update && apt-get install -y fuse && sed -i 's/^#user_allow_other$/user_allow_other/g' /etc/fuse.conf 29 | # Add a non-root user matching the nonroot user from the main container 30 | RUN groupadd -g 65532 -r nonroot && useradd -u 65532 -g 65532 -r nonroot 31 | # Set the uid as an integer for compatibility with runAsNonRoot in Kubernetes 32 | USER 65532 33 | 34 | COPY --from=build --chown=nonroot /go/src/cloudsql-proxy/cloud_sql_proxy /cloud_sql_proxy 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Report defective or unintentional behavior you've experienced. 4 | title: "Brief summary of what bug or error was observed" 5 | labels: 'type: bug' 6 | 7 | --- 8 | 9 | 25 | 26 | ## Bug Description 27 | 28 | Please enter a detailed description of the bug, and any information about what 29 | behavior you noticed and how it differs from what you expected. 30 | 31 | ## Example code (or command) 32 | 33 | ``` 34 | // example 35 | ``` 36 | 37 | ## Stacktrace 38 | ``` 39 | Any relevant stacktrace here. Be sure to filter sensitive information. 40 | ``` 41 | 42 | ## How to reproduce 43 | 44 | 1. ? 45 | 2. ? 46 | 47 | ## Environment 48 | 49 | 1. OS type and version: 50 | 2. Cloud SQL Proxy version (`./cloud_sql_proxy -version`): 51 | -------------------------------------------------------------------------------- /proxy/util/cloudsqlutil_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import "testing" 18 | 19 | func TestSplitName(t *testing.T) { 20 | table := []struct{ in, wantProj, wantRegion, wantInstance string }{ 21 | {"proj:region:my-db", "proj", "region", "my-db"}, 22 | {"google.com:project:region:instance", "google.com:project", "region", "instance"}, 23 | {"google.com:missing:part", "google.com:missing", "", "part"}, 24 | } 25 | 26 | for _, test := range table { 27 | gotProj, gotRegion, gotInstance := SplitName(test.in) 28 | if gotProj != test.wantProj { 29 | t.Errorf("splitName(%q): got %v for project, want %v", test.in, gotProj, test.wantProj) 30 | } 31 | if gotRegion != test.wantRegion { 32 | t.Errorf("splitName(%q): got %v for region, want %v", test.in, gotRegion, test.wantRegion) 33 | } 34 | if gotInstance != test.wantInstance { 35 | t.Errorf("splitName(%q): got %v for instance, want %v", test.in, gotInstance, test.wantInstance) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /proxy/dialers/mysql/hook_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package mysql_test 16 | 17 | import ( 18 | "fmt" 19 | "time" 20 | 21 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/dialers/mysql" 22 | ) 23 | 24 | // ExampleCfg shows how to use Cloud SQL Proxy dialer if you must update some 25 | // settings normally passed in the DSN such as the DBName or timeouts. 26 | func ExampleCfg() { 27 | cfg := mysql.Cfg("project:region:instance-name", "user", "") 28 | cfg.DBName = "DB_1" 29 | cfg.ParseTime = true 30 | 31 | const timeout = 10 * time.Second 32 | cfg.Timeout = timeout 33 | cfg.ReadTimeout = timeout 34 | cfg.WriteTimeout = timeout 35 | 36 | db, err := mysql.DialCfg(cfg) 37 | if err != nil { 38 | panic("couldn't dial: " + err.Error()) 39 | } 40 | // Close db after this method exits since we don't need it for the 41 | // connection pooling. 42 | defer db.Close() 43 | 44 | var now time.Time 45 | fmt.Println(db.QueryRow("SELECT NOW()").Scan(&now)) 46 | fmt.Println(now) 47 | } 48 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This is the official list of people who can contribute 2 | # (and typically have contributed) code to the repository. 3 | # The AUTHORS file lists the copyright holders; this file 4 | # lists people. For example, Google employees are listed here 5 | # but not in AUTHORS, because Google holds the copyright. 6 | # 7 | # The submission process automatically checks to make sure 8 | # that people submitting code are listed in this file (by email address). 9 | # 10 | # Names should be added to this file only after verifying that 11 | # the individual or the individual's organization has agreed to 12 | # the appropriate Contributor License Agreement, found here: 13 | # 14 | # https://cla.developers.google.com/about/google-individual 15 | # https://cla.developers.google.com/about/google-corporate 16 | # 17 | # The CLA can be filled out on the web: 18 | # 19 | # https://cla.developers.google.com/ 20 | # 21 | # When adding J Random Contributor's name to this file, 22 | # either J's name or J's organization's name should be 23 | # added to the AUTHORS file, depending on whether the 24 | # individual or corporate CLA was used. 25 | 26 | # Names should be added to this file like so: 27 | # Name 28 | # 29 | # An entry with two email addresses specifies that the 30 | # first address should be used in the submit logs and 31 | # that the second address should be recognized as the 32 | # same person when interacting with Rietveld. 33 | 34 | # Please keep the list sorted. 35 | 36 | Mykola Smith 37 | Frank van Rest 38 | Kevin Malachowski 39 | 40 | -------------------------------------------------------------------------------- /logging/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package logging contains helpers to support log messages. If you are using 16 | // the Cloud SQL Proxy as a Go library, you can override these variables to 17 | // control where log messages end up. 18 | package logging 19 | 20 | import ( 21 | "log" 22 | "os" 23 | ) 24 | 25 | // Verbosef is called to write verbose logs, such as when a new connection is 26 | // established correctly. 27 | var Verbosef = log.Printf 28 | 29 | // Infof is called to write informational logs, such as when startup has 30 | var Infof = log.Printf 31 | 32 | // Errorf is called to write an error log, such as when a new connection fails. 33 | var Errorf = log.Printf 34 | 35 | // LogDebugToStdout updates Verbosef and Info logging to use stdout instead of stderr. 36 | func LogDebugToStdout() { 37 | logger := log.New(os.Stdout, "", log.LstdFlags) 38 | Verbosef = logger.Printf 39 | Infof = logger.Printf 40 | } 41 | 42 | // LogVerboseToNowhere updates Verbosef so verbose log messages are discarded 43 | func LogVerboseToNowhere() { 44 | Verbosef = func(string, ...interface{}) {} 45 | } 46 | -------------------------------------------------------------------------------- /proxy/util/cloudsqlutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package util contains utility functions for use throughout the Cloud SQL Proxy. 16 | package util 17 | 18 | import "strings" 19 | 20 | // SplitName splits a fully qualified instance into its project, region, and 21 | // instance name components. While we make the transition to regionalized 22 | // metadata, the region is optional. 23 | // 24 | // Examples: 25 | // "proj:region:my-db" -> ("proj", "region", "my-db") 26 | // "google.com:project:region:instance" -> ("google.com:project", "region", "instance") 27 | // "google.com:missing:part" -> ("google.com:missing", "", "part") 28 | func SplitName(instance string) (project, region, name string) { 29 | spl := strings.Split(instance, ":") 30 | if len(spl) < 2 { 31 | return "", "", instance 32 | } 33 | if dot := strings.Index(spl[0], "."); dot != -1 { 34 | spl[1] = spl[0] + ":" + spl[1] 35 | spl = spl[1:] 36 | } 37 | switch { 38 | case len(spl) < 2: 39 | return "", "", instance 40 | case len(spl) == 2: 41 | return spl[0], "", spl[1] 42 | default: 43 | return spl[0], spl[1], spl[2] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/kubernetes/proxy_with_workload_identity.yaml: -------------------------------------------------------------------------------- 1 | # [START cloud_sql_proxy_k8s_sa] 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: 6 | spec: 7 | selector: 8 | matchLabels: 9 | app: 10 | template: 11 | metadata: 12 | labels: 13 | app: 14 | spec: 15 | serviceAccountName: 16 | # [END cloud_sql_proxy_k8s_sa] 17 | # [START cloud_sql_proxy_k8s_secrets] 18 | containers: 19 | - name: 20 | # ... other container configuration 21 | env: 22 | - name: DB_USER 23 | valueFrom: 24 | secretKeyRef: 25 | name: 26 | key: username 27 | - name: DB_PASS 28 | valueFrom: 29 | secretKeyRef: 30 | name: 31 | key: password 32 | - name: DB_NAME 33 | valueFrom: 34 | secretKeyRef: 35 | name: 36 | key: database 37 | # [END cloud_sql_proxy_k8s_secrets] 38 | # [START cloud_sql_proxy_k8s_container] 39 | - name: cloud-sql-proxy 40 | # It is recommended to use the latest version of the Cloud SQL proxy 41 | # Make sure to update on a regular schedule! 42 | image: gcr.io/cloudsql-docker/gce-proxy:1.17 43 | command: 44 | - "/cloud_sql_proxy" 45 | 46 | # If connecting from a VPC-native GKE cluster, you can use the 47 | # following flag to have the proxy connect over private IP 48 | # - "-ip_address_types=PRIVATE" 49 | 50 | # Replace DB_PORT with the port the proxy should listen on 51 | # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 52 | - "-instances==tcp:" 53 | securityContext: 54 | # The default Cloud SQL proxy image runs as the 55 | # "nonroot" user and group (uid: 65532) by default. 56 | runAsNonRoot: true 57 | # [END cloud_sql_proxy_k8s_container] 58 | -------------------------------------------------------------------------------- /proxy/dialers/postgres/hook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package postgres adds a 'cloudsqlpostgres' driver to use when you want 16 | // to access a Cloud SQL Database via the go database/sql library. 17 | // It is a wrapper over the driver found at github.com/lib/pq. 18 | // To use this driver, you can look at an example in 19 | // postgres_test package in the hook_test.go file 20 | package postgres 21 | 22 | import ( 23 | "database/sql" 24 | "database/sql/driver" 25 | "fmt" 26 | "net" 27 | "regexp" 28 | "time" 29 | 30 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" 31 | "github.com/lib/pq" 32 | ) 33 | 34 | func init() { 35 | sql.Register("cloudsqlpostgres", &Driver{}) 36 | } 37 | 38 | type Driver struct{} 39 | 40 | type dialer struct{} 41 | 42 | // instanceRegexp is used to parse the addr returned by lib/pq. 43 | // lib/pq returns the format '[project:region:instance]:port' 44 | var instanceRegexp = regexp.MustCompile(`^\[(.+)\]:[0-9]+$`) 45 | 46 | func (d dialer) Dial(ntw, addr string) (net.Conn, error) { 47 | matches := instanceRegexp.FindStringSubmatch(addr) 48 | if len(matches) != 2 { 49 | return nil, fmt.Errorf("failed to parse addr: %q. It should conform to the regular expression %q", addr, instanceRegexp) 50 | } 51 | instance := matches[1] 52 | return proxy.Dial(instance) 53 | } 54 | 55 | func (d dialer) DialTimeout(ntw, addr string, timeout time.Duration) (net.Conn, error) { 56 | return nil, fmt.Errorf("timeout is not currently supported for cloudsqlpostgres dialer") 57 | } 58 | 59 | func (d *Driver) Open(name string) (driver.Conn, error) { 60 | return pq.DialOpen(dialer{}, name) 61 | } 62 | -------------------------------------------------------------------------------- /.kokoro/release_artifacts.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright 2020 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # This script distributes the artifacts for the Cloud SQL proxy to their different channels. 17 | 18 | set -e # exit immediatly if any step fails 19 | 20 | PROJ_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. >/dev/null 2>&1 && pwd )" 21 | cd $PROJ_ROOT 22 | 23 | # get the current version 24 | export VERSION=$(cat version.txt) 25 | if [ -z "$VERSION" ]; then 26 | echo "error: No version.txt found in $PROJ_ROOT" 27 | exit 1 28 | fi 29 | 30 | 31 | read -p "This will release new Cloud SQL proxy artifacts for \"$VERSION\", even if they already exist. Are you sure (y/Y)? " -n 1 -r 32 | echo 33 | if [[ ! $REPLY =~ ^[Yy]$ ]] 34 | then 35 | exit 1 36 | fi 37 | 38 | # Build and push the container images 39 | gcloud builds submit --async --config .build/default.yaml --substitutions _VERSION=$VERSION 40 | gcloud builds submit --async --config .build/buster.yaml --substitutions _VERSION=$VERSION 41 | gcloud builds submit --async --config .build/alpine.yaml --substitutions _VERSION=$VERSION 42 | 43 | # Build the binarys and upload to GCS 44 | gcloud builds submit --config .build/gcs_upload.yaml --substitutions _VERSION=$VERSION 45 | # cleam up any artifacts.json left by previous builds 46 | gsutil rm -f gs://cloudsql-proxy/v$VERSION/*.json 2> /dev/null || true 47 | 48 | # Generate sha256 hashes for authentication 49 | echo -e "Add the following table to the release notes on GitHub: \n\n" 50 | echo "| filename | sha256 hash |" 51 | echo "|----------|-------------|" 52 | for f in $(gsutil ls "gs://cloudsql-proxy/v$VERSION/cloud_sql_proxy*"); do 53 | file=$(basename $f) 54 | sha=$(gsutil cat $f | sha256sum --binary) 55 | echo "| [$file](https://storage.googleapis.com/cloudsql-proxy/v$VERSION/$file) | $sha |" 56 | done 57 | -------------------------------------------------------------------------------- /examples/kubernetes/proxy_with_sa_key.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: 9 | template: 10 | metadata: 11 | labels: 12 | app: 13 | spec: 14 | containers: 15 | - name: 16 | # ... other container configuration 17 | env: 18 | - name: DB_USER 19 | valueFrom: 20 | secretKeyRef: 21 | name: 22 | key: username 23 | - name: DB_PASS 24 | valueFrom: 25 | secretKeyRef: 26 | name: 27 | key: password 28 | - name: DB_NAME 29 | valueFrom: 30 | secretKeyRef: 31 | name: 32 | key: database 33 | - name: cloud-sql-proxy 34 | # It is recommended to use the latest version of the Cloud SQL proxy 35 | # Make sure to update on a regular schedule! 36 | image: gcr.io/cloudsql-docker/gce-proxy:1.17 37 | command: 38 | - "/cloud_sql_proxy" 39 | 40 | # If connecting from a VPC-native GKE cluster, you can use the 41 | # following flag to have the proxy connect over private IP 42 | # - "-ip_address_types=PRIVATE" 43 | 44 | # Replace DB_PORT with the port the proxy should listen on 45 | # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 46 | - "-instances==tcp:" 47 | 48 | # [START cloud_sql_proxy_k8s_volume_mount] 49 | # This flag specifies where the service account key can be found 50 | - "-credential_file=/secrets/service_account.json" 51 | securityContext: 52 | # The default Cloud SQL proxy image runs as the 53 | # "nonroot" user and group (uid: 65532) by default. 54 | runAsNonRoot: true 55 | volumeMounts: 56 | - name: 57 | mountPath: /secrets/ 58 | readOnly: true 59 | # [END cloud_sql_proxy_k8s_volume_mount] 60 | # [START cloud_sql_proxy_k8s_volume_secret] 61 | volumes: 62 | - name: 63 | secret: 64 | secretName: 65 | # [START cloud_sql_proxy_k8s_volume_secret] 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Cloud SQL Proxy Hardening 3 | [中文版](https://github.com/Funny-Systems-OSS/CloudSQL-Proxy-Hardening/blob/master/README.zh.md)\ 4 | CloudSQL Proxy Hardening Encryptor: https://github.com/Funny-Systems-OSS/CloudSQL-Proxy-Hardening-Encryptor 5 | + [Problem and Solution](#Solution) 6 | + [Features](#Features) 7 | + [Requirements](#Requirements) 8 | + [Installation](#Installation) 9 | + [Usage](#Usage) 10 | + [Todo](#Todo) 11 | ## Problem and Solution 12 | ![Before](https://github.com/Funny-Systems-OSS/cloudsql-proxy-hardening/blob/master/asset/01.PNG) 13 | ![After](https://github.com/Funny-Systems-OSS/cloudsql-proxy-hardening/blob/master/asset/02.PNG) 14 | ![Result](https://github.com/Funny-Systems-OSS/cloudsql-proxy-hardening/blob/master/asset/03.PNG) 15 | ## Features 16 | + Replace plain credential file with encrypted one which bound to instance ID. 17 | + Compare with original Cloud SQL Proxy 18 | Software | Google Cloud SQL Proxy | Cloud SQL Proxy Hardening (OSS Edition) | Cloud SQL Proxy Hardening (Enterpise Edition) 19 | ---------------------|-------------------------|-----------------------------------------|---------------------------------------------- 20 | Credential File | Plaintext | **Ciphertext** | **Ciphertext** 21 | Bind with Instance | No | **Yes** | **Yes** 22 | Support Auto-Scaling | No | No | **Yes** 23 | Authentication | Service Account | Serivce Account & **Instance ID** | **ENHANCED** 24 | + For more infomation ? Contact [oss@funny.systems](mailto:oss@funny.systems) 25 | ## Requirements 26 | + Go 1.15 or higher. 27 | ## Fork from 28 | cloudsql-proxy: https://github.com/GoogleCloudPlatform/cloudsql-proxy/tree/v1.19.0 29 | ## Installation 30 | 1. git clone https://github.com/Funny-Systems-OSS/cloudsql-proxy-hardening.git 31 | 2. cd ./cloudsql-proxy-hardening 32 | 3. go build -o ../cloud_sql_proxy_funny ./cmd/cloud_sql_proxy/ 33 | ## Usage 34 | ![Overview](https://github.com/Funny-Systems-OSS/cloudsql-proxy-hardening/blob/master/asset/manual.png) 35 | + ./cloud_sql_proxy_funny <-credential_file credential_file_path> [-use_plainfile] 36 | + -credential_file:\ 37 | The encrypted credential file be used to retrieve Service Account credential in cloud_sql_proxy_funny. 38 | + -use_plainfile:\ 39 | Setting this flag will allow you to use plainfile. 40 | ## Todo 41 | + Act as classic cloud_sql_proxy if not in debug mode. 42 | -------------------------------------------------------------------------------- /tests/sqlserver_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !skip_sqlserver 16 | 17 | // sqlserver_test runs various tests against a SqlServer flavored Cloud SQL instance. 18 | package tests 19 | 20 | import ( 21 | "flag" 22 | "fmt" 23 | "os" 24 | "testing" 25 | 26 | _ "github.com/denisenkom/go-mssqldb" 27 | ) 28 | 29 | var ( 30 | sqlserverConnName = flag.String("sqlserver_conn_name", os.Getenv("SQLSERVER_CONNECTION_NAME"), "Cloud SQL SqlServer instance connection name, in the form of 'project:region:instance'.") 31 | sqlserverUser = flag.String("sqlserver_user", os.Getenv("SQLSERVER_USER"), "Name of database user.") 32 | sqlserverPass = flag.String("sqlserver_pass", os.Getenv("SQLSERVER_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") 33 | sqlserverDb = flag.String("sqlserver_db", os.Getenv("SQLSERVER_DB"), "Name of the database to connect to.") 34 | 35 | sqlserverPort = 1433 36 | ) 37 | 38 | func requireSqlserverVars(t *testing.T) { 39 | switch "" { 40 | case *sqlserverConnName: 41 | t.Fatal("'sqlserver_conn_name' not set") 42 | case *sqlserverUser: 43 | t.Fatal("'sqlserver_user' not set") 44 | case *sqlserverPass: 45 | t.Fatal("'sqlserver_pass' not set") 46 | case *sqlserverDb: 47 | t.Fatal("'sqlserver_db' not set") 48 | } 49 | } 50 | 51 | func TestSqlServerTcp(t *testing.T) { 52 | requireSqlserverVars(t) 53 | 54 | dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1:%d/%s", *sqlserverUser, *sqlserverPass, sqlserverPort, *sqlserverDb) 55 | proxyConnTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort, "") 56 | } 57 | 58 | func TestSqlserverConnLimit(t *testing.T) { 59 | requireSqlserverVars(t) 60 | 61 | dsn := fmt.Sprintf("sqlserver://%s:%s@127.0.0.1:%d/%s", *sqlserverUser, *sqlserverPass, sqlserverPort, *sqlserverDb) 62 | proxyConnLimitTest(t, *sqlserverConnName, "sqlserver", dsn, sqlserverPort) 63 | } 64 | -------------------------------------------------------------------------------- /.build/gcs_upload.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | options: 16 | env: 17 | - "GOPATH=/workspace/GOPATH" 18 | - "CGO_ENABLED=0" 19 | 20 | steps: 21 | - id: linux.amd64 22 | name: "golang:1.14" 23 | env: 24 | - "GOOS=linux" 25 | - "GOARCH=amd64" 26 | entrypoint: "bash" 27 | args: 28 | - "-c" 29 | - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' 30 | - id: linux.386 31 | name: "golang:1.14" 32 | env: 33 | - "GOOS=linux" 34 | - "GOARCH=386" 35 | entrypoint: "bash" 36 | args: 37 | - "-c" 38 | - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' 39 | - id: darwin.amd64 40 | name: "golang:1.14" 41 | env: 42 | - "GOOS=darwin" 43 | - "GOARCH=amd64" 44 | entrypoint: "bash" 45 | args: 46 | - "-c" 47 | - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' 48 | - id: darwin.386 49 | name: "golang:1.14" 50 | env: 51 | - "GOOS=darwin" 52 | - "GOARCH=386" 53 | entrypoint: "bash" 54 | args: 55 | - "-c" 56 | - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=binary.$$GOOS.$$GOARCH" -o cloud_sql_proxy.$$GOOS.$$GOARCH ./cmd/cloud_sql_proxy' 57 | - id: windows.amd64 58 | name: "golang:1.14" 59 | env: 60 | - "GOOS=windows" 61 | - "GOARCH=amd64" 62 | entrypoint: "bash" 63 | args: 64 | - "-c" 65 | - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=binary.$$GOOS.$$GOARCH" -o cloud_sql_proxy_x64.exe ./cmd/cloud_sql_proxy' 66 | - id: windows.386 67 | name: "golang:1.14" 68 | env: 69 | - "GOOS=windows" 70 | - "GOARCH=386" 71 | entrypoint: "bash" 72 | args: 73 | - "-c" 74 | - 'go build -ldflags "-X main.versionString=${_VERSION} -X main.metadataString=binary.$$GOOS.$$GOARCH" -o cloud_sql_proxy_x86.exe ./cmd/cloud_sql_proxy' 75 | artifacts: 76 | objects: 77 | location: "gs://cloudsql-proxy/v${_VERSION}/" 78 | paths: 79 | - "cloud_sql_proxy*" 80 | -------------------------------------------------------------------------------- /tests/postgres_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !skip_postgres 16 | 17 | // postgres_test runs various tests against a Postgres flavored Cloud SQL instance. 18 | package tests 19 | 20 | import ( 21 | "flag" 22 | "fmt" 23 | "io/ioutil" 24 | "log" 25 | "os" 26 | "path" 27 | "testing" 28 | 29 | _ "github.com/lib/pq" 30 | ) 31 | 32 | var ( 33 | postgresConnName = flag.String("postgres_conn_name", os.Getenv("POSTGRES_CONNECTION_NAME"), "Cloud SQL Postgres instance connection name, in the form of 'project:region:instance'.") 34 | postgresUser = flag.String("postgres_user", os.Getenv("POSTGRES_USER"), "Name of database user.") 35 | postgresPass = flag.String("postgres_pass", os.Getenv("POSTGRES_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") 36 | postgresDb = flag.String("postgres_db", os.Getenv("POSTGRES_DB"), "Name of the database to connect to.") 37 | 38 | postgresPort = 5432 39 | ) 40 | 41 | func requirePostgresVars(t *testing.T) { 42 | switch "" { 43 | case *postgresConnName: 44 | t.Fatal("'postgres_conn_name' not set") 45 | case *postgresUser: 46 | t.Fatal("'postgres_user' not set") 47 | case *postgresPass: 48 | t.Fatal("'postgres_pass' not set") 49 | case *postgresDb: 50 | t.Fatal("'postgres_db' not set") 51 | } 52 | } 53 | func TestPostgresTcp(t *testing.T) { 54 | requirePostgresVars(t) 55 | 56 | dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) 57 | proxyConnTest(t, *postgresConnName, "postgres", dsn, postgresPort, "") 58 | } 59 | 60 | func TestPostgresSocket(t *testing.T) { 61 | requirePostgresVars(t) 62 | 63 | dir, err := ioutil.TempDir("", "csql-proxy") 64 | if err != nil { 65 | log.Fatalf("unable to create tmp dir: %s", err) 66 | } 67 | defer os.RemoveAll(dir) 68 | 69 | dsn := fmt.Sprintf("user=%s password=%s database=%s host=%s", *postgresUser, *postgresPass, *postgresDb, path.Join(dir, *postgresConnName)) 70 | proxyConnTest(t, *postgresConnName, "postgres", dsn, 0, dir) 71 | } 72 | 73 | func TestPostgresConnLimit(t *testing.T) { 74 | requirePostgresVars(t) 75 | 76 | dsn := fmt.Sprintf("user=%s password=%s database=%s sslmode=disable", *postgresUser, *postgresPass, *postgresDb) 77 | proxyConnLimitTest(t, *postgresConnName, "postgres", dsn, postgresPort) 78 | } 79 | -------------------------------------------------------------------------------- /proxy/proxy/common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file contains tests for common.go 16 | 17 | package proxy 18 | 19 | import ( 20 | "net" 21 | "reflect" 22 | "testing" 23 | ) 24 | 25 | var c1, c2, c3 = &dummyConn{}, &dummyConn{}, &dummyConn{} 26 | 27 | type dummyConn struct{ net.Conn } 28 | 29 | func (c dummyConn) Close() error { 30 | return nil 31 | } 32 | 33 | func TestConnSetAdd(t *testing.T) { 34 | s := NewConnSet() 35 | 36 | s.Add("a", c1) 37 | aSlice := []string{"a"} 38 | if !reflect.DeepEqual(s.IDs(), aSlice) { 39 | t.Fatalf("got %v, want %v", s.IDs(), aSlice) 40 | } 41 | 42 | s.Add("a", c2) 43 | if !reflect.DeepEqual(s.IDs(), aSlice) { 44 | t.Fatalf("got %v, want %v", s.IDs(), aSlice) 45 | } 46 | 47 | s.Add("b", c3) 48 | ids := s.IDs() 49 | if len(ids) != 2 { 50 | t.Fatalf("got %d ids, wanted 2", len(ids)) 51 | } 52 | ok := ids[0] == "a" && ids[1] == "b" || 53 | ids[1] == "a" && ids[0] == "b" 54 | 55 | if !ok { 56 | t.Fatalf(`got %v, want only "a" and "b"`, ids) 57 | } 58 | } 59 | 60 | func TestConnSetRemove(t *testing.T) { 61 | s := NewConnSet() 62 | 63 | s.Add("a", c1) 64 | s.Add("a", c2) 65 | s.Add("b", c3) 66 | 67 | s.Remove("b", c3) 68 | if got := s.Conns("b"); got != nil { 69 | t.Fatalf("got %v, want nil", got) 70 | } 71 | 72 | aSlice := []string{"a"} 73 | if !reflect.DeepEqual(s.IDs(), aSlice) { 74 | t.Fatalf("got %v, want %v", s.IDs(), aSlice) 75 | } 76 | 77 | s.Remove("a", c1) 78 | if !reflect.DeepEqual(s.IDs(), aSlice) { 79 | t.Fatalf("got %v, want %v", s.IDs(), aSlice) 80 | } 81 | 82 | s.Remove("a", c2) 83 | if len(s.IDs()) != 0 { 84 | t.Fatalf("got %v, want empty set", s.IDs()) 85 | } 86 | } 87 | 88 | func TestConns(t *testing.T) { 89 | s := NewConnSet() 90 | 91 | s.Add("a", c1) 92 | s.Add("a", c2) 93 | s.Add("b", c3) 94 | 95 | got := s.Conns("b") 96 | if !reflect.DeepEqual(got, []net.Conn{c3}) { 97 | t.Fatalf("got %v, wanted only %v", got, c3) 98 | } 99 | 100 | looking := map[net.Conn]bool{ 101 | c1: true, 102 | c2: true, 103 | c3: true, 104 | } 105 | 106 | for _, v := range s.Conns("a", "b") { 107 | if _, ok := looking[v]; !ok { 108 | t.Errorf("got unexpected conn %v", v) 109 | } 110 | delete(looking, v) 111 | } 112 | if len(looking) != 0 { 113 | t.Fatalf("didn't find %v in list of Conns", looking) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/mysql_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !skip_mysql 16 | 17 | // mysql_test runs various tests against a MySQL flavored Cloud SQL instance. 18 | package tests 19 | 20 | import ( 21 | "flag" 22 | "io/ioutil" 23 | "log" 24 | "os" 25 | "path" 26 | "testing" 27 | 28 | mysql "github.com/go-sql-driver/mysql" 29 | ) 30 | 31 | var ( 32 | mysqlConnName = flag.String("mysql_conn_name", os.Getenv("MYSQL_CONNECTION_NAME"), "Cloud SQL MYSQL instance connection name, in the form of 'project:region:instance'.") 33 | mysqlUser = flag.String("mysql_user", os.Getenv("MYSQL_USER"), "Name of database user.") 34 | mysqlPass = flag.String("mysql_pass", os.Getenv("MYSQL_PASS"), "Password for the database user; be careful when entering a password on the command line (it may go into your terminal's history).") 35 | mysqlDb = flag.String("mysql_db", os.Getenv("MYSQL_DB"), "Name of the database to connect to.") 36 | 37 | mysqlPort = 3306 38 | ) 39 | 40 | func requireMysqlVars(t *testing.T) { 41 | switch "" { 42 | case *mysqlConnName: 43 | t.Fatal("'mysql_conn_name' not set") 44 | case *mysqlUser: 45 | t.Fatal("'mysql_user' not set") 46 | case *mysqlPass: 47 | t.Fatal("'mysql_pass' not set") 48 | case *mysqlDb: 49 | t.Fatal("'mysql_db' not set") 50 | } 51 | } 52 | 53 | func TestMysqlTcp(t *testing.T) { 54 | requireMysqlVars(t) 55 | cfg := mysql.Config{ 56 | User: *mysqlUser, 57 | Passwd: *mysqlPass, 58 | DBName: *mysqlDb, 59 | AllowNativePasswords: true, 60 | } 61 | proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort, "") 62 | } 63 | 64 | func TestMysqlSocket(t *testing.T) { 65 | requireMysqlVars(t) 66 | 67 | dir, err := ioutil.TempDir("", "csql-proxy-tests") 68 | if err != nil { 69 | log.Fatalf("unable to create tmp dir: %s", err) 70 | } 71 | defer os.RemoveAll(dir) 72 | 73 | cfg := mysql.Config{ 74 | User: *mysqlUser, 75 | Passwd: *mysqlPass, 76 | Net: "unix", 77 | Addr: path.Join(dir, *mysqlConnName), 78 | DBName: *mysqlDb, 79 | AllowNativePasswords: true, 80 | } 81 | proxyConnTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), 0, dir) 82 | } 83 | 84 | func TestMysqlConnLimit(t *testing.T) { 85 | requireMysqlVars(t) 86 | cfg := mysql.Config{ 87 | User: *mysqlUser, 88 | Passwd: *mysqlPass, 89 | DBName: *mysqlDb, 90 | AllowNativePasswords: true, 91 | } 92 | proxyConnLimitTest(t, *mysqlConnName, "mysql", cfg.FormatDSN(), mysqlPort) 93 | } 94 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.19.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.18.0...v1.19.0) (2020-11-18) 4 | 5 | 6 | ### Features 7 | 8 | * Added DialContext to Client and proxy package ([#483](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/483)) ([c84aa50](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c84aa5079668e07e3d2dc8f254d30e1103a6ead3)) 9 | * use regionalized instance ids to prevent global conflicts with sqladmin v1 ([#504](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/504)) ([6c45513](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6c455136a24b841dbfc015a1f8ed7505f9e77dec)) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * **containers:** Allow non-root users to mount fuse filesystems for alpine and buster images ([#540](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/540)) ([5b653f5](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/5b653f5df6d9c4c226e3c4f6036d5e7d4c43c699)) 15 | * only allow fuse mode to unmount if an error occurs first ([#537](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/537)) ([6caef36](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/6caef36968d23b931c824450e418e29ac6277191)) 16 | * refreshCfg no longer caches error over valid cert ([#521](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/521)) ([4a6b3d8](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/4a6b3d8c895e2634afd8cee2341db668f20b9a33)) 17 | 18 | ## [1.18.0](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/compare/v1.17.0...v1.18.0) (2020-09-08) 19 | 20 | 21 | ### Features 22 | 23 | * **containers:** Add "-alpine" and "-buster" based images. ([#415](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/415)) ([ebcf294](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/ebcf294b9ee028340695868fb6f4cc4bbe09d849)) 24 | * **containers:** Add fuse to alpine and buster images ([#459](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/459)) ([0f28fcd](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/0f28fcd008a5bb863ec2ca1402c31ae81d7dae5d)) 25 | 26 | 27 | ### Bug Fixes 28 | * Print out any errors during SIGTERM-caused shutdown ([#389](https://github.com/GoogleCloudPlatform/cloudsql-proxy/pull/389)) 29 | * Optimize `-term-timeout` wait ([#391](https://github.com/GoogleCloudPlatform/cloudsql-proxy/pull/391)) 30 | * Add socket suffix for Postgres instances when running in `-fuse` mode ([#426](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/426)) ([20ffaec](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/20ffaec2f0f00a2516206a0453bd0d1c6e62770c)) 31 | * **containers:** Specify nonroot user by uid to work with runAsNonRoot ([#402](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/issues/402)) ([c5c0be1](https://www.github.com/GoogleCloudPlatform/cloudsql-proxy/commit/c5c0be1b60bfc1c3fa862039619908a328066e5e)) 32 | * Releases are now tagged using `vMAJOR.MINOR.PATCH` for correct compatibility with go-modules. Please note that this will effect container image tags (which were previously only `vMAJOR.MINOR`), since these tags correspond directly to the release on GitHub. 33 | -------------------------------------------------------------------------------- /proxy/limits/limits.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !windows,!freebsd 16 | 17 | // Package limits provides routines to check and enforce certain resource 18 | // limits on the Cloud SQL client proxy process. 19 | package limits 20 | 21 | import ( 22 | "fmt" 23 | "syscall" 24 | 25 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 26 | ) 27 | 28 | var ( 29 | // For overriding in unittests. 30 | syscallGetrlimit = syscall.Getrlimit 31 | syscallSetrlimit = syscall.Setrlimit 32 | ) 33 | 34 | // Each connection handled by the proxy requires two file descriptors, one 35 | // for the local end of the connection and one for the remote. So, the proxy 36 | // process should be able to open at least 8K file descriptors if it is to 37 | // handle 4K connections to one instance. 38 | const ExpectedFDs = 8500 39 | 40 | // SetupFDLimits ensures that the process running the Cloud SQL proxy can have 41 | // at least wantFDs number of open file descriptors. It returns an error if it 42 | // cannot ensure the same. 43 | func SetupFDLimits(wantFDs uint64) error { 44 | rlim := &syscall.Rlimit{} 45 | if err := syscallGetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { 46 | return fmt.Errorf("failed to read rlimit for max file descriptors: %v", err) 47 | } 48 | 49 | if rlim.Cur >= wantFDs { 50 | logging.Verbosef("current FDs rlimit set to %d, wanted limit is %d. Nothing to do here.", rlim.Cur, wantFDs) 51 | return nil 52 | } 53 | 54 | // Linux man page: 55 | // The soft limit is the value that the kernel enforces for the corre‐ 56 | // sponding resource. The hard limit acts as a ceiling for the soft limit: 57 | // an unprivileged process may set only its soft limit to a value in the 58 | // range from 0 up to the hard limit, and (irreversibly) lower its hard 59 | // limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE 60 | // capability in the initial user namespace) may make arbitrary changes to 61 | // either limit value. 62 | if rlim.Max < wantFDs { 63 | // When the hard limit is less than what is requested, let's just give it a 64 | // shot, and if we fail, we fallback and try just setting the softlimit. 65 | rlim2 := &syscall.Rlimit{} 66 | rlim2.Max = wantFDs 67 | rlim2.Cur = wantFDs 68 | if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim2); err == nil { 69 | logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim2) 70 | return nil 71 | } 72 | } 73 | 74 | rlim.Cur = wantFDs 75 | if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { 76 | return fmt.Errorf("failed to set rlimit {%v} for max file descriptors: %v", rlim, err) 77 | } 78 | 79 | logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim) 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /proxy/limits/limits_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build freebsd 16 | 17 | // Package limits provides routines to check and enforce certain resource 18 | // limits on the Cloud SQL client proxy process. 19 | package limits 20 | 21 | import ( 22 | "fmt" 23 | "syscall" 24 | 25 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 26 | ) 27 | 28 | var ( 29 | // For overriding in unittests. 30 | syscallGetrlimit = syscall.Getrlimit 31 | syscallSetrlimit = syscall.Setrlimit 32 | ) 33 | 34 | // Each connection handled by the proxy requires two file descriptors, one 35 | // for the local end of the connection and one for the remote. So, the proxy 36 | // process should be able to open at least 8K file descriptors if it is to 37 | // handle 4K connections to one instance. 38 | const ExpectedFDs = 8500 39 | 40 | // SetupFDLimits ensures that the process running the Cloud SQL proxy can have 41 | // at least wantFDs number of open file descriptors. It returns an error if it 42 | // cannot ensure the same. 43 | func SetupFDLimits(wantFDs uint64) error { 44 | rlim := &syscall.Rlimit{} 45 | if err := syscallGetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { 46 | return fmt.Errorf("failed to read rlimit for max file descriptors: %v", err) 47 | } 48 | 49 | if uint64(rlim.Cur) >= wantFDs { 50 | logging.Verbosef("current FDs rlimit set to %d, wanted limit is %d. Nothing to do here.", rlim.Cur, wantFDs) 51 | return nil 52 | } 53 | 54 | // Linux man page: 55 | // The soft limit is the value that the kernel enforces for the corre‐ 56 | // sponding resource. The hard limit acts as a ceiling for the soft limit: 57 | // an unprivileged process may set only its soft limit to a value in the 58 | // range from 0 up to the hard limit, and (irreversibly) lower its hard 59 | // limit. A privileged process (under Linux: one with the CAP_SYS_RESOURCE 60 | // capability in the initial user namespace) may make arbitrary changes to 61 | // either limit value. 62 | if uint64(rlim.Max) < wantFDs { 63 | // When the hard limit is less than what is requested, let's just give it a 64 | // shot, and if we fail, we fallback and try just setting the softlimit. 65 | rlim2 := &syscall.Rlimit{} 66 | rlim2.Max = int64(wantFDs) 67 | rlim2.Cur = int64(wantFDs) 68 | if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim2); err == nil { 69 | logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim2) 70 | return nil 71 | } 72 | } 73 | 74 | rlim.Cur = int64(wantFDs) 75 | if err := syscallSetrlimit(syscall.RLIMIT_NOFILE, rlim); err != nil { 76 | return fmt.Errorf("failed to set rlimit {%v} for max file descriptors: %v", rlim, err) 77 | } 78 | 79 | logging.Verbosef("Rlimits for file descriptors set to {%v}", rlim) 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /proxy/dialers/mysql/hook.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package mysql adds a 'cloudsql' network to use when you want to access a 16 | // Cloud SQL Database via the mysql driver found at 17 | // github.com/go-sql-driver/mysql. It also exposes helper functions for 18 | // dialing. 19 | package mysql 20 | 21 | import ( 22 | "database/sql" 23 | "errors" 24 | 25 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" 26 | "github.com/go-sql-driver/mysql" 27 | ) 28 | 29 | func init() { 30 | mysql.RegisterDialContext("cloudsql", proxy.DialContext) 31 | } 32 | 33 | // Dial logs into the specified Cloud SQL Instance using the given user and no 34 | // password. To set more options, consider calling DialCfg instead. 35 | // 36 | // The provided instance should be in the form project-name:region:instance-name. 37 | // 38 | // The returned *sql.DB may be valid even if there's also an error returned 39 | // (e.g. if there was a transient connection error). 40 | func Dial(instance, user string) (*sql.DB, error) { 41 | cfg := mysql.NewConfig() 42 | cfg.User = user 43 | cfg.Addr = instance 44 | return DialCfg(cfg) 45 | } 46 | 47 | // DialPassword is similar to Dial, but allows you to specify a password. 48 | // 49 | // Note that using a password with the proxy is not necessary as long as the 50 | // user's hostname in the mysql.user table is 'cloudsqlproxy~'. For more 51 | // information, see: 52 | // https://cloud.google.com/sql/docs/sql-proxy#user 53 | func DialPassword(instance, user, password string) (*sql.DB, error) { 54 | cfg := mysql.NewConfig() 55 | cfg.User = user 56 | cfg.Passwd = password 57 | cfg.Addr = instance 58 | return DialCfg(cfg) 59 | } 60 | 61 | // Cfg returns the effective *mysql.Config to represent connectivity to the 62 | // provided instance via the given user and password. The config can be 63 | // modified and passed to DialCfg to connect. If you don't modify the returned 64 | // config before dialing, consider using Dial or DialPassword. 65 | func Cfg(instance, user, password string) *mysql.Config { 66 | cfg := mysql.NewConfig() 67 | cfg.User = user 68 | cfg.Passwd = password 69 | cfg.Addr = instance 70 | cfg.Net = "cloudsql" 71 | return cfg 72 | } 73 | 74 | // DialCfg opens up a SQL connection to a Cloud SQL Instance specified by the 75 | // provided configuration. It is otherwise the same as Dial. 76 | // 77 | // The cfg.Addr should be the instance's connection string, in the format of: 78 | // project-name:region:instance-name. 79 | func DialCfg(cfg *mysql.Config) (*sql.DB, error) { 80 | if cfg.TLSConfig != "" { 81 | return nil, errors.New("do not specify TLS when using the Proxy") 82 | } 83 | 84 | // Copy the config so that we can modify it without feeling bad. 85 | c := *cfg 86 | c.Net = "cloudsql" 87 | dsn := c.FormatDSN() 88 | 89 | db, err := sql.Open("mysql", dsn) 90 | if err == nil { 91 | err = db.Ping() 92 | } 93 | return db, err 94 | } 95 | -------------------------------------------------------------------------------- /proxy/util/gcloudutil.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "os/exec" 23 | "runtime" 24 | "time" 25 | 26 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 27 | "golang.org/x/oauth2" 28 | ) 29 | 30 | // GcloudConfigData represents the data returned by `gcloud config config-helper`. 31 | type GcloudConfigData struct { 32 | Configuration struct { 33 | Properties struct { 34 | Core struct { 35 | Project string 36 | Account string 37 | } 38 | } 39 | } 40 | Credential struct { 41 | AccessToken string `json:"access_token"` 42 | TokenExpiry time.Time `json:"token_expiry"` 43 | } 44 | } 45 | 46 | func (cfg *GcloudConfigData) oauthToken() *oauth2.Token { 47 | return &oauth2.Token{ 48 | AccessToken: cfg.Credential.AccessToken, 49 | Expiry: cfg.Credential.TokenExpiry, 50 | } 51 | } 52 | 53 | type GcloudStatusCode int 54 | 55 | const ( 56 | GcloudOk GcloudStatusCode = iota 57 | GcloudNotFound 58 | // generic execution failure error not specified above. 59 | GcloudExecErr 60 | ) 61 | 62 | type GcloudError struct { 63 | GcloudError error 64 | Status GcloudStatusCode 65 | } 66 | 67 | func (e *GcloudError) Error() string { 68 | return e.GcloudError.Error() 69 | } 70 | 71 | // GcloudConfig returns a GcloudConfigData object or an error of type *GcloudError. 72 | func GcloudConfig() (*GcloudConfigData, error) { 73 | gcloudCmd := "gcloud" 74 | if runtime.GOOS == "windows" { 75 | gcloudCmd = gcloudCmd + ".cmd" 76 | } 77 | 78 | if _, err := exec.LookPath(gcloudCmd); err != nil { 79 | return nil, &GcloudError{err, GcloudNotFound} 80 | } 81 | 82 | buf, errbuf := new(bytes.Buffer), new(bytes.Buffer) 83 | cmd := exec.Command(gcloudCmd, "--format", "json", "config", "config-helper") 84 | cmd.Stdout = buf 85 | cmd.Stderr = errbuf 86 | 87 | if err := cmd.Run(); err != nil { 88 | err = fmt.Errorf("error reading config: %v; stderr was:\n%v", err, errbuf) 89 | logging.Errorf("GcloudConfig: %v", err) 90 | return nil, &GcloudError{err, GcloudExecErr} 91 | } 92 | 93 | data := &GcloudConfigData{} 94 | if err := json.Unmarshal(buf.Bytes(), data); err != nil { 95 | logging.Errorf("Failed to unmarshal bytes from gcloud: %v", err) 96 | logging.Errorf(" gcloud returned:\n%s", buf) 97 | return nil, &GcloudError{err, GcloudExecErr} 98 | } 99 | 100 | return data, nil 101 | } 102 | 103 | // gcloudTokenSource implements oauth2.TokenSource via the `gcloud config config-helper` command. 104 | type gcloudTokenSource struct { 105 | } 106 | 107 | // Token helps gcloudTokenSource implement oauth2.TokenSource. 108 | func (src *gcloudTokenSource) Token() (*oauth2.Token, error) { 109 | cfg, err := GcloudConfig() 110 | if err != nil { 111 | return nil, err 112 | } 113 | return cfg.oauthToken(), nil 114 | } 115 | 116 | func GcloudTokenSource(ctx context.Context) (oauth2.TokenSource, error) { 117 | cfg, err := GcloudConfig() 118 | if err != nil { 119 | return nil, err 120 | } 121 | return oauth2.ReuseTokenSource(cfg.oauthToken(), &gcloudTokenSource{}), nil 122 | } 123 | -------------------------------------------------------------------------------- /tests/connection_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // connection_test.go provides some helpers for basic connectivity tests to Cloud SQL instances. 16 | package tests 17 | 18 | import ( 19 | "context" 20 | "database/sql" 21 | "fmt" 22 | "sync" 23 | "testing" 24 | ) 25 | 26 | // proxyConnTest is a test helper to verify the proxy works with a basic connectivity test. 27 | func proxyConnTest(t *testing.T, connName, driver, dsn string, port int, dir string) { 28 | ctx := context.Background() 29 | 30 | var args []string 31 | if dir != "" { // unix port 32 | args = append(args, fmt.Sprintf("-dir=%s", dir), fmt.Sprintf("-instances=%s", connName)) 33 | } else { // tcp socket 34 | args = append(args, fmt.Sprintf("-instances=%s=tcp:%d", connName, port)) 35 | } 36 | 37 | // Start the proxy 38 | p, err := StartProxy(ctx, args...) 39 | if err != nil { 40 | t.Fatalf("unable to start proxy: %v", err) 41 | } 42 | defer p.Close() 43 | output, err := p.WaitForServe(ctx) 44 | if err != nil { 45 | t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) 46 | } 47 | 48 | // Connect to the instance 49 | db, err := sql.Open(driver, dsn) 50 | if err != nil { 51 | t.Fatalf("unable to connect to db: %s", err) 52 | } 53 | defer db.Close() 54 | _, err = db.Exec("SELECT 1;") 55 | if err != nil { 56 | 57 | t.Fatalf("unable to exec on db: %s", err) 58 | } 59 | } 60 | 61 | func proxyConnLimitTest(t *testing.T, connName, driver, dsn string, port int) { 62 | ctx := context.Background() 63 | 64 | maxConn, totConn := 5, 10 65 | 66 | // Start the proxy 67 | p, err := StartProxy(ctx, fmt.Sprintf("-instances=%s=tcp:%d", connName, port), fmt.Sprintf("-max_connections=%d", maxConn)) 68 | if err != nil { 69 | t.Fatalf("unable to start proxy: %v", err) 70 | } 71 | defer p.Close() 72 | output, err := p.WaitForServe(ctx) 73 | if err != nil { 74 | t.Fatalf("unable to verify proxy was serving: %s \n %s", err, output) 75 | } 76 | 77 | // Create connection pool 78 | var stmt string 79 | switch driver { 80 | case "mysql": 81 | stmt = "SELECT sleep(2);" 82 | case "postgres": 83 | stmt = "SELECT pg_sleep(2);" 84 | case "sqlserver": 85 | stmt = "WAITFOR DELAY '00:00:02'" 86 | default: 87 | t.Fatalf("unsupported driver: no sleep query found") 88 | } 89 | db, err := sql.Open(driver, dsn) 90 | if err != nil { 91 | t.Fatalf("unable to connect to db: %s", err) 92 | } 93 | db.SetMaxIdleConns(0) 94 | defer db.Close() 95 | 96 | // Connect with up to totConn and count errors 97 | var wg sync.WaitGroup 98 | c := make(chan error, totConn) 99 | for i := 0; i < totConn; i++ { 100 | wg.Add(1) 101 | go func() { 102 | defer wg.Done() 103 | _, err = db.ExecContext(ctx, stmt) 104 | if err != nil { 105 | c <- err 106 | } 107 | }() 108 | } 109 | wg.Wait() 110 | close(c) 111 | 112 | var errs []error 113 | for e := range c { 114 | errs = append(errs, e) 115 | } 116 | want, got := totConn-maxConn, len(errs) 117 | if want != got { 118 | t.Errorf("wrong errCt - want: %d, got %d", want, got) 119 | for _, e := range errs { 120 | t.Errorf("%s\n", e) 121 | } 122 | t.Fail() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /proxy/proxy/dial.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package proxy 16 | 17 | import ( 18 | "fmt" 19 | "net" 20 | "net/http" 21 | "sync" 22 | 23 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/certs" 24 | "golang.org/x/net/context" 25 | "golang.org/x/oauth2/google" 26 | ) 27 | 28 | const port = 3307 29 | 30 | var dialClient struct { 31 | // This client is initialized in Init/InitWithClient/InitDefault 32 | // and read in Dial. 33 | c *Client 34 | sync.Mutex 35 | } 36 | 37 | // Dial returns a net.Conn connected to the Cloud SQL Instance specified. The 38 | // format of 'instance' is "project-name:region:instance-name". 39 | // 40 | // If one of the Init functions hasn't been called yet, InitDefault is called. 41 | // 42 | // This is a network-level function; consider looking in the dialers 43 | // subdirectory for more convenience functions related to actually logging into 44 | // your database. 45 | func DialContext(ctx context.Context, instance string) (net.Conn, error) { 46 | dialClient.Lock() 47 | c := dialClient.c 48 | dialClient.Unlock() 49 | if c == nil { 50 | if err := InitDefault(ctx); err != nil { 51 | return nil, fmt.Errorf("default proxy initialization failed; consider calling proxy.Init explicitly: %v", err) 52 | } 53 | // InitDefault initialized the client. 54 | dialClient.Lock() 55 | c = dialClient.c 56 | dialClient.Unlock() 57 | } 58 | 59 | return c.DialContext(ctx, instance) 60 | 61 | } 62 | 63 | // Dial does the same as DialContext but using context.Background() as the context. 64 | func Dial(instance string) (net.Conn, error) { 65 | return DialContext(context.Background(), instance) 66 | } 67 | 68 | // Dialer is a convenience type to model the standard 'Dial' function. 69 | type Dialer func(net, addr string) (net.Conn, error) 70 | 71 | // Init must be called before Dial is called. This is a more flexible version 72 | // of InitDefault, but allows you to set more fields. 73 | // 74 | // The http.Client is used to authenticate API requests. 75 | // The connset parameter is optional. 76 | // If the dialer is nil, net.Conn is used. 77 | // Use InitWithClient to with a filled client if you want to provide a Context-Aware dialer 78 | func Init(auth *http.Client, connset *ConnSet, dialer Dialer) { 79 | dialClient.Lock() 80 | dialClient.c = &Client{ 81 | Port: port, 82 | Certs: certs.NewCertSource("", auth, true), 83 | Conns: connset, 84 | Dialer: dialer, 85 | } 86 | dialClient.Unlock() 87 | } 88 | 89 | // InitClient is similar to Init, but allows you to specify the Client 90 | // directly. 91 | 92 | // Deprecated: Use InitWithClient instead. 93 | func InitClient(c Client) { 94 | dialClient.Lock() 95 | dialClient.c = &c 96 | dialClient.Unlock() 97 | } 98 | 99 | // InitWithClient specifies the Client directly. 100 | func InitWithClient(c *Client) { 101 | dialClient.Lock() 102 | dialClient.c = c 103 | dialClient.Unlock() 104 | } 105 | 106 | // InitDefault attempts to initialize the Dial function using application 107 | // default credentials. 108 | func InitDefault(ctx context.Context) error { 109 | cl, err := google.DefaultClient(ctx, "https://www.googleapis.com/auth/sqlservice.admin") 110 | if err != nil { 111 | return err 112 | } 113 | Init(cl, nil, nil) 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /proxy/limits/limits_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !windows 16 | 17 | package limits 18 | 19 | import ( 20 | "errors" 21 | "math" 22 | "syscall" 23 | "testing" 24 | ) 25 | 26 | type rlimitFunc func(int, *syscall.Rlimit) error 27 | 28 | func TestSetupFDLimits(t *testing.T) { 29 | tests := []struct { 30 | desc string 31 | getFunc rlimitFunc 32 | setFunc rlimitFunc 33 | wantFDs uint64 34 | wantErr bool 35 | }{ 36 | { 37 | desc: "Getrlimit fails", 38 | getFunc: func(_ int, _ *syscall.Rlimit) error { 39 | return errors.New("failed to read rlimit for max file descriptors") 40 | }, 41 | setFunc: func(_ int, _ *syscall.Rlimit) error { 42 | panic("shouldn't be called") 43 | }, 44 | wantFDs: 0, 45 | wantErr: true, 46 | }, 47 | { 48 | desc: "Getrlimit max is less than wantFDs", 49 | getFunc: func(_ int, rlim *syscall.Rlimit) error { 50 | rlim.Cur = 512 51 | rlim.Max = 512 52 | return nil 53 | }, 54 | setFunc: func(_ int, rlim *syscall.Rlimit) error { 55 | if rlim.Cur != 1024 || rlim.Max != 1024 { 56 | return errors.New("setrlimit called with unexpected value") 57 | } 58 | return nil 59 | }, 60 | wantFDs: 1024, 61 | wantErr: false, 62 | }, 63 | { 64 | desc: "Getrlimit returns rlim_infinity", 65 | getFunc: func(_ int, rlim *syscall.Rlimit) error { 66 | rlim.Cur = math.MaxUint64 67 | rlim.Max = math.MaxUint64 68 | return nil 69 | }, 70 | setFunc: func(_ int, _ *syscall.Rlimit) error { 71 | panic("shouldn't be called") 72 | }, 73 | wantFDs: 1024, 74 | wantErr: false, 75 | }, 76 | { 77 | desc: "Getrlimit cur is greater than wantFDs", 78 | getFunc: func(_ int, rlim *syscall.Rlimit) error { 79 | rlim.Cur = 512 80 | rlim.Max = 512 81 | return nil 82 | }, 83 | setFunc: func(_ int, _ *syscall.Rlimit) error { 84 | panic("shouldn't be called") 85 | }, 86 | wantFDs: 256, 87 | wantErr: false, 88 | }, 89 | { 90 | desc: "Setrlimit fails", 91 | getFunc: func(_ int, rlim *syscall.Rlimit) error { 92 | rlim.Cur = 128 93 | rlim.Max = 512 94 | return nil 95 | }, 96 | setFunc: func(_ int, _ *syscall.Rlimit) error { 97 | return errors.New("failed to set rlimit for max file descriptors") 98 | }, 99 | wantFDs: 256, 100 | wantErr: true, 101 | }, 102 | { 103 | desc: "Success", 104 | getFunc: func(_ int, rlim *syscall.Rlimit) error { 105 | rlim.Cur = 128 106 | rlim.Max = 512 107 | return nil 108 | }, 109 | setFunc: func(_ int, _ *syscall.Rlimit) error { 110 | return nil 111 | }, 112 | wantFDs: 256, 113 | wantErr: false, 114 | }, 115 | } 116 | 117 | for _, test := range tests { 118 | oldGetFunc := syscallGetrlimit 119 | syscallGetrlimit = test.getFunc 120 | defer func() { 121 | syscallGetrlimit = oldGetFunc 122 | }() 123 | 124 | oldSetFunc := syscallSetrlimit 125 | syscallSetrlimit = test.setFunc 126 | defer func() { 127 | syscallSetrlimit = oldSetFunc 128 | }() 129 | 130 | gotErr := SetupFDLimits(test.wantFDs) 131 | if (gotErr != nil) != test.wantErr { 132 | t.Errorf("%s: limits.SetupFDLimits(%d) returned error %v, wantErr %v", test.desc, test.wantFDs, gotErr, test.wantErr) 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package tests contains end to end tests meant to verify the Cloud SQL Proxy 16 | // works as expected when executed as a binary. 17 | // 18 | // Required flags: 19 | // -mysql_conn_name, -db_user, -db_pass 20 | package tests 21 | 22 | import ( 23 | "bufio" 24 | "bytes" 25 | "context" 26 | "flag" 27 | "fmt" 28 | "io" 29 | "io/ioutil" 30 | "log" 31 | "os" 32 | "os/exec" 33 | "path" 34 | "runtime" 35 | "strings" 36 | "testing" 37 | ) 38 | 39 | var ( 40 | binPath = "" 41 | ) 42 | 43 | func TestMain(m *testing.M) { 44 | flag.Parse() 45 | // compile the proxy as a binary 46 | var err error 47 | binPath, err = compileProxy() 48 | if err != nil { 49 | log.Fatalf("failed to compile proxy: %s", err) 50 | } 51 | // Run tests and cleanup 52 | rtn := m.Run() 53 | os.RemoveAll(binPath) 54 | 55 | os.Exit(rtn) 56 | } 57 | 58 | // compileProxy compiles the binary into a temporary directory, and returns the path to the file or any error that occured. 59 | func compileProxy() (string, error) { 60 | // get path of the cmd pkg 61 | _, f, _, ok := runtime.Caller(0) 62 | if !ok { 63 | return "", fmt.Errorf("failed to find cmd pkg") 64 | } 65 | projRoot := path.Dir(path.Dir(f)) // cd ../.. 66 | pkgPath := path.Join(projRoot, "cmd", "cloud_sql_proxy") 67 | // compile the proxy into a tmp directory 68 | tmp, err := ioutil.TempDir("", "") 69 | if err != nil { 70 | return "", fmt.Errorf("failed to create temp dir: %s", err) 71 | } 72 | b := path.Join(tmp, "cloud_sql_proxy") 73 | cmd := exec.Command("go", "build", "-o", b, pkgPath) 74 | out, err := cmd.CombinedOutput() 75 | if err != nil { 76 | return "", fmt.Errorf("failed to run 'go build': %w \n %s", err, out) 77 | } 78 | return b, nil 79 | } 80 | 81 | // proxyExec represents an execution of the Cloud SQL proxy. 82 | type ProxyExec struct { 83 | Out io.ReadCloser 84 | 85 | cmd *exec.Cmd 86 | cancel context.CancelFunc 87 | closers []io.Closer 88 | done chan bool // closed once the cmd is completed 89 | err error 90 | } 91 | 92 | // StartProxy returns a proxyExec representing a running instance of the proxy. 93 | func StartProxy(ctx context.Context, args ...string) (*ProxyExec, error) { 94 | var err error 95 | ctx, cancel := context.WithCancel(ctx) 96 | p := ProxyExec{ 97 | cmd: exec.CommandContext(ctx, binPath, args...), 98 | cancel: cancel, 99 | done: make(chan bool), 100 | } 101 | pr, pw, err := os.Pipe() 102 | if err != nil { 103 | return nil, fmt.Errorf("unable to open stdout pipe: %w", err) 104 | } 105 | defer pw.Close() 106 | p.Out, p.cmd.Stdout, p.cmd.Stderr = pr, pw, pw 107 | p.closers = append(p.closers, pr) 108 | if err := p.cmd.Start(); err != nil { 109 | defer p.Close() 110 | return nil, fmt.Errorf("unable to start cmd: %w", err) 111 | } 112 | // when process is complete, mark as finished 113 | go func() { 114 | defer close(p.done) 115 | p.err = p.cmd.Wait() 116 | }() 117 | return &p, nil 118 | } 119 | 120 | // Stop sends the pskill signal to the proxy and returns. 121 | func (p *ProxyExec) Kill() { 122 | p.cancel() 123 | } 124 | 125 | // Waits until the execution is completed and returns any error. 126 | func (p *ProxyExec) Wait() error { 127 | select { 128 | case <-p.done: 129 | return p.err 130 | } 131 | } 132 | 133 | // Stop sends the pskill signal to the proxy and returns. 134 | func (p *ProxyExec) Done() bool { 135 | select { 136 | case <-p.done: 137 | return true 138 | default: 139 | } 140 | return false 141 | } 142 | 143 | // Close releases any resources assotiated with the instance. 144 | func (p *ProxyExec) Close() { 145 | p.cancel() 146 | for _, c := range p.closers { 147 | c.Close() 148 | } 149 | } 150 | 151 | // WaitForServe waits until the proxy ready to serve traffic. Returns any output from the proxy 152 | // while starting or any errors experienced before the proxy was ready to server. 153 | func (p *ProxyExec) WaitForServe(ctx context.Context) (output string, err error) { 154 | // Watch for the "Ready for new connections" to indicate the proxy is listening 155 | buf, in, errCh := new(bytes.Buffer), bufio.NewReader(p.Out), make(chan error, 1) 156 | go func() { 157 | defer close(errCh) 158 | for { 159 | // if ctx is finished, stop processing 160 | select { 161 | case <-ctx.Done(): 162 | return 163 | default: 164 | } 165 | s, err := in.ReadString('\n') 166 | if err != nil { 167 | errCh <- err 168 | return 169 | } 170 | buf.WriteString(s) 171 | if strings.Contains(s, "Ready for new connections") { 172 | errCh <- nil 173 | return 174 | } 175 | } 176 | }() 177 | // Wait for either the background thread of the context to complete 178 | select { 179 | case <-ctx.Done(): 180 | return buf.String(), fmt.Errorf("context done: %w", ctx.Err()) 181 | case err := <-errCh: 182 | if err != nil { 183 | return buf.String(), fmt.Errorf("proxy start failed: %w", err) 184 | } 185 | } 186 | return buf.String(), nil 187 | } 188 | -------------------------------------------------------------------------------- /proxy/fuse/fuse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !windows 16 | 17 | package fuse 18 | 19 | import ( 20 | "bytes" 21 | "io" 22 | "io/ioutil" 23 | "log" 24 | "net" 25 | "os" 26 | "path/filepath" 27 | "sync" 28 | "syscall" 29 | "testing" 30 | 31 | "bazil.org/fuse" 32 | ) 33 | 34 | var ( 35 | dir = filepath.Join(os.TempDir(), "cloudsql") 36 | tmpdir = filepath.Join(os.TempDir(), "cloudsql-tmp") 37 | ) 38 | 39 | func TestFuseClose(t *testing.T) { 40 | src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | if err := fuse.Close(); err != nil { 46 | t.Fatal(err) 47 | } 48 | if got, ok := <-src; ok { 49 | t.Fatalf("got new connection %#v, expected closed source", got) 50 | } 51 | } 52 | 53 | // TestBadDir verifies that the fuse module does not create directories, only simple files. 54 | func TestBadDir(t *testing.T) { 55 | _, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | defer fuse.Close() 60 | 61 | _, err = os.Stat(filepath.Join(dir, "dir1", "dir2")) 62 | if err == nil { 63 | t.Fatal("able to find a directory inside the mount point, expected only regular files") 64 | } 65 | if err := err.(*os.PathError); err.Err != syscall.ENOTDIR { 66 | t.Fatalf("got %#v, want ENOTDIR (%v)", err.Err, syscall.ENOTDIR) 67 | } 68 | } 69 | 70 | func TestReadme(t *testing.T) { 71 | _, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | defer fuse.Close() 76 | 77 | data, err := ioutil.ReadFile(filepath.Join(dir, "README")) 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | // We just care that the file exists. Print out the contents for 82 | // informational purposes. 83 | t.Log(string(data)) 84 | } 85 | 86 | func TestSingleInstance(t *testing.T) { 87 | src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | defer fuse.Close() 92 | 93 | const want = "test:instance" 94 | path := filepath.Join(dir, want) 95 | 96 | fi, err := os.Stat(path) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | if fi.Mode()&os.ModeType != os.ModeSocket { 102 | t.Fatalf("%q had mode %v (%X), expected a socket file", path, fi.Mode(), uint32(fi.Mode())) 103 | } 104 | 105 | c, err := net.Dial("unix", path) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | defer c.Close() 110 | 111 | got, ok := <-src 112 | if !ok { 113 | t.Fatal("connection source was closed, expected a connection") 114 | } else if got.Instance != want { 115 | t.Fatalf("got %q, want %q", got.Instance, want) 116 | } else if got.Conn == nil { 117 | t.Fatal("got nil connection, wanted a connection") 118 | } 119 | 120 | const sent = "test string" 121 | go func() { 122 | if _, err := c.Write([]byte(sent)); err != nil { 123 | t.Error(err) 124 | } 125 | if err := c.Close(); err != nil { 126 | t.Error(err) 127 | } 128 | }() 129 | 130 | gotData := new(bytes.Buffer) 131 | if _, err := io.Copy(gotData, got.Conn); err != nil { 132 | t.Fatal(err) 133 | } else if gotData.String() != sent { 134 | t.Fatalf("got %q, want %v", gotData.String(), sent) 135 | } 136 | } 137 | 138 | func BenchmarkNewConnection(b *testing.B) { 139 | src, fuse, err := NewConnSrc(dir, tmpdir, nil, nil) 140 | if err != nil { 141 | b.Fatal(err) 142 | } 143 | 144 | const want = "X" 145 | incomingCount := 0 146 | var incoming sync.Mutex // Is unlocked when the following goroutine exits. 147 | go func() { 148 | incoming.Lock() 149 | defer incoming.Unlock() 150 | 151 | for c := range src { 152 | c.Conn.Write([]byte(want)) 153 | c.Conn.Close() 154 | incomingCount++ 155 | } 156 | }() 157 | 158 | const instance = "test:instance" 159 | path := filepath.Join(dir, instance) 160 | 161 | b.ResetTimer() 162 | for i := 0; i < b.N; i++ { 163 | c, err := net.Dial("unix", path) 164 | if err != nil { 165 | b.Errorf("couldn't dial: %v", err) 166 | } 167 | 168 | data, err := ioutil.ReadAll(c) 169 | if err != nil { 170 | b.Errorf("got read error: %v", err) 171 | } else if got := string(data); got != want { 172 | b.Errorf("read %q, want %q", string(data), want) 173 | } 174 | } 175 | if err := fuse.Close(); err != nil { 176 | b.Fatal(err) 177 | } 178 | 179 | // Wait for the 'incoming' goroutine to finish. 180 | incoming.Lock() 181 | if incomingCount != b.N { 182 | b.Fatalf("got %d connections, want %d", incomingCount, b.N) 183 | } 184 | } 185 | 186 | func TestMain(m *testing.M) { 187 | // Ensure this directory exists. 188 | os.MkdirAll(dir, 0777) 189 | 190 | // Unmount before the tests start, else they won't work correctly. 191 | if err := fuse.Unmount(dir); err != nil { 192 | log.Printf("couldn't unmount fuse directory %q: %v", dir, err) 193 | } 194 | 195 | ret := m.Run() 196 | // Make sure to unmount at the end, so that we don't leave the system in an 197 | // inconsistent state in case something weird happened. 198 | if err := fuse.Unmount(dir); err != nil { 199 | log.Printf("couldn't unmount fuse directory %q: %v", dir, err) 200 | } 201 | 202 | os.Exit(ret) 203 | } 204 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. **Please sign one of the contributor license agreements below!** 4 | 1. Fork the repo, develop and test your code changes, add docs. 5 | 1. Make sure that your commit messages clearly describe the changes. 6 | 1. Send a pull request. 7 | 8 | ## Table of contents 9 | * [Opening an issue](#opening-an-issue) 10 | * [Contributor License Agreements](#contributor-license-agreements) 11 | * [Contributor Code of Conduct](#contributor-code-of-conduct) 12 | 13 | ## Opening an issue 14 | 15 | If you find a bug in the proxy code or an inaccuracy in the documentation, 16 | please open an issue. GitHub provides a guide, [Mastering 17 | Issues](https://guides.github.com/features/issues/), that is useful if you are 18 | unfamiliar with the process. Here are the specific steps for opening an issue: 19 | 20 | 1. Go to the project issues page on GitHub. 21 | 1. Click the green `New Issue` button located in the upper right corner. 22 | 1. In the title field, write a single phrase that identifies your issue. 23 | 1. In the main editor, describe your issue. 24 | 1. Click the submit button. 25 | 26 | Thank you. We will do our best to triage your issue within one business day, and 27 | attempt to categorize your issues with an estimate of the priority and issue 28 | type. We will try to respond with regular updates based on its priority: 29 | 30 | * **Critical** respond and update daily, resolve with a week 31 | * **High** respond and update weekly, resolve within six weeks 32 | * **Medium** respond and update every three months, best effort resolution 33 | * **Low** respond and update every six months, best effort resolution 34 | 35 | The priority we assign will be roughly a function of the number of users we 36 | expect to be impacted, as well as its severity. As a rule of thumb: 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
SeverityNumber of users
HandfulSomeMostAll
Easy, obvious workaroundLowLowMediumHigh 57 |
Non-obvious workaround availableLowMediumHighCritical
Functionality blockedHighHighCriticalCritical
74 | 75 | ## Contributor License Agreements 76 | 77 | Open-source software licensing is a wonderful arrangement that benefits 78 | everyone, but in an imperfect world, we all need to exercise some legal 79 | prudence. In order to protect you, Google, and most of all, everyone who comes 80 | to depend on these libraries, we require that all contributors sign our short 81 | and human-readable Contributor License Agreement (CLA). We don't want to open 82 | the door to patent trolls, predatory lawyers, or anyone else who isn't on board 83 | with creating value and making the world a better place. We hope you will agree 84 | that the CLA offers very important protection and is easy to understand. Take a 85 | moment to read it carefully, and if you agree with what you read, please sign it 86 | now. If you believe you've already signed the appropriate CLA already for this 87 | or any other Google open-source project, you shouldn't have to do so again. You 88 | can review your signed CLAs at 89 | [cla.developers.google.com/clas](https://cla.developers.google.com/clas). 90 | 91 | First, check that you are signed in to a [Google 92 | Account](https://accounts.google.com) that matches your [local Git email 93 | address](https://help.github.com/articles/setting-your-email-in-git/). Then 94 | choose one of the following: 95 | 96 | * If you are **an individual writing original source code** and **you own the 97 | intellectual property**, sign the [Individual 98 | CLA](https://developers.google.com/open-source/cla/individual). 99 | * If you work for **a company that wants to allow you to contribute**, sign the 100 | [Corporate CLA](https://developers.google.com/open-source/cla/corporate). 101 | 102 | You (and your authorized signer, if corporate) can sign the CLA 103 | electronically. After that, we'll be able to accept your contributions. 104 | 105 | ## Contributor Code of Conduct 106 | 107 | As contributors and maintainers of this project, and in the interest of 108 | fostering an open and welcoming community, we pledge to respect all people who 109 | contribute through reporting issues, posting feature requests, updating 110 | documentation, submitting pull requests or patches, and other activities. 111 | 112 | We are committed to making participation in this project a harassment-free 113 | experience for everyone, regardless of level of experience, gender, gender 114 | identity and expression, sexual orientation, disability, personal appearance, 115 | body size, race, ethnicity, age, religion, or nationality. 116 | 117 | Examples of unacceptable behavior by participants include: 118 | 119 | * The use of sexualized language or imagery 120 | * Personal attacks 121 | * Trolling or insulting/derogatory comments 122 | * Public or private harassment 123 | * Publishing other's private information, such as physical or electronic 124 | addresses, without explicit permission 125 | * Other unethical or unprofessional conduct. 126 | 127 | Project maintainers have the right and responsibility to remove, edit, or reject 128 | comments, commits, code, wiki edits, issues, and other contributions that are 129 | not aligned to this Code of Conduct. By adopting this Code of Conduct, project 130 | maintainers commit themselves to fairly and consistently applying these 131 | principles to every aspect of managing this project. Project maintainers who do 132 | not follow or enforce the Code of Conduct may be permanently removed from the 133 | project team. 134 | 135 | This code of conduct applies both within project spaces and in public spaces 136 | when an individual is representing the project or its community. 137 | 138 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 139 | reported by opening an issue or contacting one or more of the project 140 | maintainers. 141 | 142 | This Code of Conduct is adapted from the [Contributor 143 | Covenant](http://contributor-covenant.org), version 1.2.0, available at 144 | [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) 145 | -------------------------------------------------------------------------------- /proxy/proxy/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package proxy implements client and server code for proxying an unsecure connection over SSL. 16 | package proxy 17 | 18 | import ( 19 | "bytes" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "net" 24 | "sync" 25 | 26 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 27 | ) 28 | 29 | // SQLScope is the Google Cloud Platform scope required for executing API 30 | // calls to Cloud SQL. 31 | const SQLScope = "https://www.googleapis.com/auth/sqlservice.admin" 32 | 33 | type dbgConn struct { 34 | net.Conn 35 | } 36 | 37 | func (d dbgConn) Write(b []byte) (int, error) { 38 | x, y := d.Conn.Write(b) 39 | logging.Verbosef("write(%q) => (%v, %v)", b, x, y) 40 | return x, y 41 | } 42 | 43 | func (d dbgConn) Read(b []byte) (int, error) { 44 | x, y := d.Conn.Read(b) 45 | logging.Verbosef("read: (%v, %v) => %q", x, y, b[:x]) 46 | return x, y 47 | } 48 | 49 | func (d dbgConn) Close() error { 50 | err := d.Conn.Close() 51 | logging.Verbosef("close: %v", err) 52 | return err 53 | } 54 | 55 | // myCopy is similar to io.Copy, but reports whether the returned error was due 56 | // to a bad read or write. The returned error will never be nil 57 | func myCopy(dst io.Writer, src io.Reader) (readErr bool, err error) { 58 | buf := make([]byte, 4096) 59 | for { 60 | n, err := src.Read(buf) 61 | if n > 0 { 62 | if _, werr := dst.Write(buf[:n]); werr != nil { 63 | if err == nil { 64 | return false, werr 65 | } 66 | // Read and write error; just report read error (it happened first). 67 | return true, err 68 | } 69 | } 70 | if err != nil { 71 | return true, err 72 | } 73 | } 74 | } 75 | 76 | func copyError(readDesc, writeDesc string, readErr bool, err error) { 77 | var desc string 78 | if readErr { 79 | desc = "Reading data from " + readDesc 80 | } else { 81 | desc = "Writing data to " + writeDesc 82 | } 83 | logging.Errorf("%v had error: %v", desc, err) 84 | } 85 | 86 | func copyThenClose(remote, local io.ReadWriteCloser, remoteDesc, localDesc string) { 87 | firstErr := make(chan error, 1) 88 | 89 | go func() { 90 | readErr, err := myCopy(remote, local) 91 | select { 92 | case firstErr <- err: 93 | if readErr && err == io.EOF { 94 | logging.Verbosef("Client closed %v", localDesc) 95 | } else { 96 | copyError(localDesc, remoteDesc, readErr, err) 97 | } 98 | remote.Close() 99 | local.Close() 100 | default: 101 | } 102 | }() 103 | 104 | readErr, err := myCopy(local, remote) 105 | select { 106 | case firstErr <- err: 107 | if readErr && err == io.EOF { 108 | logging.Verbosef("Instance %v closed connection", remoteDesc) 109 | } else { 110 | copyError(remoteDesc, localDesc, readErr, err) 111 | } 112 | remote.Close() 113 | local.Close() 114 | default: 115 | // In this case, the other goroutine exited first and already printed its 116 | // error (and closed the things). 117 | } 118 | } 119 | 120 | // NewConnSet initializes a new ConnSet and returns it. 121 | func NewConnSet() *ConnSet { 122 | return &ConnSet{m: make(map[string][]net.Conn)} 123 | } 124 | 125 | // A ConnSet tracks net.Conns associated with a provided ID. 126 | // A nil ConnSet will be a no-op for all methods called on it. 127 | type ConnSet struct { 128 | sync.RWMutex 129 | m map[string][]net.Conn 130 | } 131 | 132 | // String returns a debug string for the ConnSet. 133 | func (c *ConnSet) String() string { 134 | if c == nil { 135 | return "" 136 | } 137 | var b bytes.Buffer 138 | 139 | c.RLock() 140 | for id, conns := range c.m { 141 | fmt.Fprintf(&b, "ID %s:", id) 142 | for i, c := range conns { 143 | fmt.Fprintf(&b, "\n\t%d: %v", i, c) 144 | } 145 | } 146 | c.RUnlock() 147 | 148 | return b.String() 149 | } 150 | 151 | // Add saves the provided conn and associates it with the given string 152 | // identifier. 153 | func (c *ConnSet) Add(id string, conn net.Conn) { 154 | if c == nil { 155 | return 156 | } 157 | c.Lock() 158 | c.m[id] = append(c.m[id], conn) 159 | c.Unlock() 160 | } 161 | 162 | // IDs returns a slice of all identifiers which still have active connections. 163 | func (c *ConnSet) IDs() []string { 164 | if c == nil { 165 | return nil 166 | } 167 | ret := make([]string, 0, len(c.m)) 168 | 169 | c.RLock() 170 | for k := range c.m { 171 | ret = append(ret, k) 172 | } 173 | c.RUnlock() 174 | 175 | return ret 176 | } 177 | 178 | // Conns returns all active connections associated with the provided ids. 179 | func (c *ConnSet) Conns(ids ...string) []net.Conn { 180 | if c == nil { 181 | return nil 182 | } 183 | var ret []net.Conn 184 | 185 | c.RLock() 186 | for _, id := range ids { 187 | ret = append(ret, c.m[id]...) 188 | } 189 | c.RUnlock() 190 | 191 | return ret 192 | } 193 | 194 | // Remove undoes an Add operation to have the set forget about a conn. Do not 195 | // Remove an id/conn pair more than it has been Added. 196 | func (c *ConnSet) Remove(id string, conn net.Conn) error { 197 | if c == nil { 198 | return nil 199 | } 200 | c.Lock() 201 | defer c.Unlock() 202 | 203 | pos := -1 204 | conns := c.m[id] 205 | for i, cc := range conns { 206 | if cc == conn { 207 | pos = i 208 | break 209 | } 210 | } 211 | 212 | if pos == -1 { 213 | return fmt.Errorf("couldn't find connection %v for id %s", conn, id) 214 | } 215 | 216 | if len(conns) == 1 { 217 | delete(c.m, id) 218 | } else { 219 | c.m[id] = append(conns[:pos], conns[pos+1:]...) 220 | } 221 | 222 | return nil 223 | } 224 | 225 | // Close closes every net.Conn contained in the set. 226 | func (c *ConnSet) Close() error { 227 | if c == nil { 228 | return nil 229 | } 230 | var errs bytes.Buffer 231 | 232 | c.Lock() 233 | for id, conns := range c.m { 234 | for _, c := range conns { 235 | if err := c.Close(); err != nil { 236 | fmt.Fprintf(&errs, "%s close error: %v\n", id, err) 237 | } 238 | } 239 | } 240 | c.Unlock() 241 | 242 | if errs.Len() == 0 { 243 | return nil 244 | } 245 | 246 | return errors.New(errs.String()) 247 | } 248 | -------------------------------------------------------------------------------- /cmd/cloud_sql_proxy/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io/ioutil" 21 | "net" 22 | "net/http" 23 | "os" 24 | "testing" 25 | ) 26 | 27 | type mockTripper struct { 28 | } 29 | 30 | func (m *mockTripper) RoundTrip(r *http.Request) (*http.Response, error) { 31 | return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewReader([]byte("{}")))}, nil 32 | } 33 | 34 | var mockClient = &http.Client{Transport: &mockTripper{}} 35 | 36 | func TestCreateInstanceConfigs(t *testing.T) { 37 | for _, v := range []struct { 38 | desc string 39 | //inputs 40 | dir string 41 | useFuse bool 42 | instances []string 43 | instancesSrc string 44 | 45 | // We don't need to check the []instancesConfig return value, we already 46 | // have a TestParseInstanceConfig. 47 | wantErr bool 48 | 49 | skipFailedInstanceConfig bool 50 | }{ 51 | { 52 | "setting -fuse and -dir", 53 | "dir", true, nil, "", false, false, 54 | }, { 55 | "setting -fuse", 56 | "", true, nil, "", true, false, 57 | }, { 58 | "setting -fuse, -dir, and -instances", 59 | "dir", true, []string{"proj:reg:x"}, "", true, false, 60 | }, { 61 | "setting -fuse, -dir, and -instances_metadata", 62 | "dir", true, nil, "md", true, false, 63 | }, { 64 | "setting -dir and -instances (unix socket)", 65 | "dir", false, []string{"proj:reg:x"}, "", false, false, 66 | }, { 67 | // tests for the case where invalid configs can still exist, when skipped 68 | "setting -dir and -instances (unix socket) w/ something invalid", 69 | "dir", false, []string{"proj:reg:x", "INVALID_PROJECT_STRING"}, "", false, true, 70 | }, { 71 | "Seting -instance (unix socket)", 72 | "", false, []string{"proj:reg:x"}, "", true, false, 73 | }, { 74 | "setting -instance (tcp socket)", 75 | "", false, []string{"proj:reg:x=tcp:1234"}, "", false, false, 76 | }, { 77 | "setting -instance (tcp socket) and -instances_metadata", 78 | "", false, []string{"proj:reg:x=tcp:1234"}, "md", true, false, 79 | }, { 80 | "setting -dir, -instance (tcp socket), and -instances_metadata", 81 | "dir", false, []string{"proj:reg:x=tcp:1234"}, "md", false, false, 82 | }, { 83 | "setting -dir, -instance (unix socket), and -instances_metadata", 84 | "dir", false, []string{"proj:reg:x"}, "md", false, false, 85 | }, { 86 | "setting -dir and -instances_metadata", 87 | "dir", false, nil, "md", false, false, 88 | }, { 89 | "setting -instances_metadata", 90 | "", false, nil, "md", true, false, 91 | }, 92 | } { 93 | _, err := CreateInstanceConfigs(v.dir, v.useFuse, v.instances, v.instancesSrc, mockClient, v.skipFailedInstanceConfig) 94 | if v.wantErr { 95 | if err == nil { 96 | t.Errorf("CreateInstanceConfigs passed when %s, wanted error", v.desc) 97 | } 98 | continue 99 | } 100 | if err != nil { 101 | t.Errorf("CreateInstanceConfigs gave error when %s: %v", v.desc, err) 102 | } 103 | } 104 | } 105 | 106 | func TestParseInstanceConfig(t *testing.T) { 107 | // sentinel values 108 | var ( 109 | anyLoopbackAddress = "" 110 | wantErr = instanceConfig{"", "", ""} 111 | ) 112 | 113 | tcs := []struct { 114 | // inputs 115 | dir, instance string 116 | 117 | wantCfg instanceConfig 118 | }{ 119 | { 120 | "/x", "domain.com:my-proj:my-reg:my-instance", 121 | instanceConfig{"domain.com:my-proj:my-reg:my-instance", "unix", "/x/domain.com:my-proj:my-reg:my-instance"}, 122 | }, { 123 | "/x", "my-proj:my-reg:my-instance", 124 | instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/my-proj:my-reg:my-instance"}, 125 | }, { 126 | "/x", "my-proj:my-reg:my-instance=unix:socket_name", 127 | instanceConfig{"my-proj:my-reg:my-instance", "unix", "/x/socket_name"}, 128 | }, { 129 | "/x", "my-proj:my-reg:my-instance=unix:/my/custom/sql-socket", 130 | instanceConfig{"my-proj:my-reg:my-instance", "unix", "/my/custom/sql-socket"}, 131 | }, { 132 | "/x", "my-proj:my-reg:my-instance=tcp:1234", 133 | instanceConfig{"my-proj:my-reg:my-instance", "tcp", anyLoopbackAddress}, 134 | }, { 135 | "/x", "my-proj:my-reg:my-instance=tcp4:1234", 136 | instanceConfig{"my-proj:my-reg:my-instance", "tcp4", "127.0.0.1:1234"}, 137 | }, { 138 | "/x", "my-proj:my-reg:my-instance=tcp6:1234", 139 | instanceConfig{"my-proj:my-reg:my-instance", "tcp6", "[::1]:1234"}, 140 | }, { 141 | "/x", "my-proj:my-reg:my-instance=tcp:my-host:1111", 142 | instanceConfig{"my-proj:my-reg:my-instance", "tcp", "my-host:1111"}, 143 | }, { 144 | "/x", "my-proj:my-reg:my-instance=", 145 | wantErr, 146 | }, { 147 | "/x", "my-proj:my-reg:my-instance=cool network", 148 | wantErr, 149 | }, { 150 | "/x", "my-proj:my-reg:my-instance=cool network:1234", 151 | wantErr, 152 | }, { 153 | "/x", "my-proj:my-reg:my-instance=oh:so:many:colons", 154 | wantErr, 155 | }, 156 | } 157 | 158 | for _, tc := range tcs { 159 | t.Run(fmt.Sprintf("parseInstanceConfig(%q, %q)", tc.dir, tc.instance), func(t *testing.T) { 160 | if os.Getenv("EXPECT_IPV4_AND_IPV6") != "true" { 161 | // Skip ipv4 and ipv6 if they are not supported by the machine. 162 | // (assumption is that validNets isn't buggy) 163 | if tc.wantCfg.Network == "tcp4" || tc.wantCfg.Network == "tcp6" { 164 | if !validNets[tc.wantCfg.Network] { 165 | t.Skipf("%q net not supported, skipping", tc.wantCfg.Network) 166 | } 167 | } 168 | } 169 | 170 | got, err := parseInstanceConfig(tc.dir, tc.instance, mockClient) 171 | if tc.wantCfg == wantErr { 172 | if err != nil { 173 | return // pass. an error was expected and returned. 174 | } 175 | t.Fatalf("parseInstanceConfig(%s, %s) = %+v, wanted error", tc.dir, tc.instance, got) 176 | } 177 | if err != nil { 178 | t.Fatalf("parseInstanceConfig(%s, %s) had unexpected error: %v", tc.dir, tc.instance, err) 179 | } 180 | 181 | if tc.wantCfg.Address == anyLoopbackAddress { 182 | host, _, err := net.SplitHostPort(got.Address) 183 | if err != nil { 184 | t.Fatalf("net.SplitHostPort(%v): %v", got.Address, err) 185 | } 186 | ip := net.ParseIP(host) 187 | if !ip.IsLoopback() { 188 | t.Fatalf("want loopback, got addr: %v", got.Address) 189 | } 190 | 191 | // use a placeholder address, so the rest of the config can be compared 192 | got.Address = "" 193 | tc.wantCfg.Address = got.Address 194 | } 195 | 196 | if got != tc.wantCfg { 197 | t.Errorf("parseInstanceConfig(%s, %s) = %+v, want %+v", tc.dir, tc.instance, got, tc.wantCfg) 198 | } 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /proxy/proxy/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package proxy 16 | 17 | import ( 18 | "context" 19 | "crypto/tls" 20 | "crypto/x509" 21 | "errors" 22 | "fmt" 23 | "net" 24 | "sync" 25 | "sync/atomic" 26 | "testing" 27 | "time" 28 | ) 29 | 30 | const instance = "instance-name" 31 | 32 | var ( 33 | errFakeDial = errors.New("this error is returned by the dialer") 34 | forever = time.Date(9999, 0, 0, 0, 0, 0, 0, time.UTC) 35 | ) 36 | 37 | type fakeCerts struct { 38 | sync.Mutex 39 | called int 40 | } 41 | 42 | type blockingCertSource struct { 43 | values map[string]*fakeCerts 44 | validUntil time.Time 45 | } 46 | 47 | func (cs *blockingCertSource) Local(instance string) (tls.Certificate, error) { 48 | v, ok := cs.values[instance] 49 | if !ok { 50 | return tls.Certificate{}, fmt.Errorf("test setup failure: unknown instance %q", instance) 51 | } 52 | v.Lock() 53 | v.called++ 54 | v.Unlock() 55 | 56 | // Returns a cert which is valid forever. 57 | return tls.Certificate{ 58 | Leaf: &x509.Certificate{ 59 | NotAfter: cs.validUntil, 60 | }, 61 | }, nil 62 | } 63 | 64 | func (cs *blockingCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { 65 | return &x509.Certificate{}, "fake address", "fake name", "fake version", nil 66 | } 67 | 68 | func TestContextDialer(t *testing.T) { 69 | b := &fakeCerts{} 70 | c := &Client{ 71 | Certs: &blockingCertSource{ 72 | map[string]*fakeCerts{ 73 | instance: b, 74 | }, 75 | forever, 76 | }, 77 | ContextDialer: func(context.Context, string, string) (net.Conn, error) { 78 | return nil, errFakeDial 79 | }, 80 | Dialer: func(string, string) (net.Conn, error) { 81 | return nil, fmt.Errorf("this dialer should't be used when ContextDialer is set") 82 | }, 83 | } 84 | 85 | if _, err := c.DialContext(context.Background(), instance); err != errFakeDial { 86 | t.Errorf("unexpected error: %v", err) 87 | } 88 | } 89 | 90 | func TestClientCache(t *testing.T) { 91 | b := &fakeCerts{} 92 | c := &Client{ 93 | Certs: &blockingCertSource{ 94 | map[string]*fakeCerts{ 95 | instance: b, 96 | }, 97 | forever, 98 | }, 99 | Dialer: func(string, string) (net.Conn, error) { 100 | return nil, errFakeDial 101 | }, 102 | } 103 | 104 | for i := 0; i < 5; i++ { 105 | if _, err := c.Dial(instance); err != errFakeDial { 106 | t.Errorf("unexpected error: %v", err) 107 | } 108 | } 109 | 110 | b.Lock() 111 | if b.called != 1 { 112 | t.Errorf("called %d times, want called 1 time", b.called) 113 | } 114 | b.Unlock() 115 | } 116 | 117 | func TestConcurrentRefresh(t *testing.T) { 118 | b := &fakeCerts{} 119 | c := &Client{ 120 | Certs: &blockingCertSource{ 121 | map[string]*fakeCerts{ 122 | instance: b, 123 | }, 124 | forever, 125 | }, 126 | Dialer: func(string, string) (net.Conn, error) { 127 | return nil, errFakeDial 128 | }, 129 | } 130 | 131 | ch := make(chan error) 132 | b.Lock() 133 | 134 | const numDials = 20 135 | 136 | for i := 0; i < numDials; i++ { 137 | go func() { 138 | _, err := c.Dial(instance) 139 | ch <- err 140 | }() 141 | } 142 | 143 | b.Unlock() 144 | 145 | for i := 0; i < numDials; i++ { 146 | if err := <-ch; err != errFakeDial { 147 | t.Errorf("unexpected error: %v", err) 148 | } 149 | } 150 | b.Lock() 151 | if b.called != 1 { 152 | t.Errorf("called %d times, want called 1 time", b.called) 153 | } 154 | b.Unlock() 155 | } 156 | 157 | func TestMaximumConnectionsCount(t *testing.T) { 158 | const maxConnections = 10 159 | const numConnections = maxConnections + 1 160 | var dials uint64 = 0 161 | 162 | b := &fakeCerts{} 163 | certSource := blockingCertSource{ 164 | map[string]*fakeCerts{}, 165 | forever, 166 | } 167 | firstDialExited := make(chan struct{}) 168 | c := &Client{ 169 | Certs: &certSource, 170 | Dialer: func(string, string) (net.Conn, error) { 171 | atomic.AddUint64(&dials, 1) 172 | 173 | // Wait until the first dial fails to ensure the max connections count is reached by a concurrent dialer 174 | <-firstDialExited 175 | 176 | return nil, errFakeDial 177 | }, 178 | MaxConnections: maxConnections, 179 | } 180 | 181 | // Build certSource.values before creating goroutines to avoid concurrent map read and map write 182 | instanceNames := make([]string, numConnections) 183 | for i := 0; i < numConnections; i++ { 184 | // Vary instance name to bypass config cache and avoid second call to Client.tryConnect() in Client.Dial() 185 | instanceName := fmt.Sprintf("%s-%d", instance, i) 186 | certSource.values[instanceName] = b 187 | instanceNames[i] = instanceName 188 | } 189 | 190 | var wg sync.WaitGroup 191 | var firstDialOnce sync.Once 192 | for _, instanceName := range instanceNames { 193 | wg.Add(1) 194 | go func(instanceName string) { 195 | defer wg.Done() 196 | 197 | conn := Conn{ 198 | Instance: instanceName, 199 | Conn: &dummyConn{}, 200 | } 201 | c.handleConn(conn) 202 | 203 | firstDialOnce.Do(func() { close(firstDialExited) }) 204 | }(instanceName) 205 | } 206 | 207 | wg.Wait() 208 | 209 | switch { 210 | case dials > maxConnections: 211 | t.Errorf("client should have refused to dial new connection on %dth attempt when the maximum of %d connections was reached (%d dials)", numConnections, maxConnections, dials) 212 | case dials == maxConnections: 213 | t.Logf("client has correctly refused to dial new connection on %dth attempt when the maximum of %d connections was reached (%d dials)\n", numConnections, maxConnections, dials) 214 | case dials < maxConnections: 215 | t.Errorf("client should have dialed exactly the maximum of %d connections (%d connections, %d dials)", maxConnections, numConnections, dials) 216 | } 217 | } 218 | 219 | func TestShutdownTerminatesEarly(t *testing.T) { 220 | b := &fakeCerts{} 221 | c := &Client{ 222 | Certs: &blockingCertSource{ 223 | map[string]*fakeCerts{ 224 | instance: b, 225 | }, 226 | forever, 227 | }, 228 | Dialer: func(string, string) (net.Conn, error) { 229 | return nil, nil 230 | }, 231 | } 232 | shutdown := make(chan bool, 1) 233 | go func() { 234 | c.Shutdown(1) 235 | shutdown <- true 236 | }() 237 | shutdownFinished := false 238 | // In case the code is actually broken and the client doesn't shut down quickly, don't cause the test to hang until it times out. 239 | select { 240 | case <-time.After(100 * time.Millisecond): 241 | case shutdownFinished = <-shutdown: 242 | } 243 | if !shutdownFinished { 244 | t.Errorf("shutdown should have completed quickly because there are no active connections") 245 | } 246 | } 247 | 248 | func TestRefreshTimer(t *testing.T) { 249 | timeToExpire := 5 * time.Second 250 | b := &fakeCerts{} 251 | certCreated := time.Now() 252 | c := &Client{ 253 | Certs: &blockingCertSource{ 254 | map[string]*fakeCerts{ 255 | instance: b, 256 | }, 257 | certCreated.Add(timeToExpire), 258 | }, 259 | Dialer: func(string, string) (net.Conn, error) { 260 | return nil, errFakeDial 261 | }, 262 | RefreshCfgThrottle: 20 * time.Millisecond, 263 | RefreshCfgBuffer: time.Second, 264 | } 265 | // Call Dial to cache the cert. 266 | if _, err := c.Dial(instance); err != errFakeDial { 267 | t.Fatalf("Dial(%s) failed: %v", instance, err) 268 | } 269 | c.cacheL.Lock() 270 | cfg, ok := c.cfgCache[instance] 271 | c.cacheL.Unlock() 272 | if !ok { 273 | t.Fatalf("expected instance to be cached") 274 | } 275 | 276 | time.Sleep(timeToExpire - time.Since(certCreated)) 277 | // Check if cert was refreshed in the background, without calling Dial again. 278 | c.cacheL.Lock() 279 | newCfg, ok := c.cfgCache[instance] 280 | c.cacheL.Unlock() 281 | if !ok { 282 | t.Fatalf("expected instance to be cached") 283 | } 284 | if !newCfg.lastRefreshed.After(cfg.lastRefreshed) { 285 | t.Error("expected cert to be refreshed.") 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /proxy/certs/certs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package certs implements a CertSource which speaks to the public Cloud SQL API endpoint. 16 | package certs 17 | 18 | import ( 19 | "crypto/rand" 20 | "crypto/rsa" 21 | "crypto/tls" 22 | "crypto/x509" 23 | "encoding/pem" 24 | "errors" 25 | "fmt" 26 | "math" 27 | mrand "math/rand" 28 | "net/http" 29 | "strings" 30 | "time" 31 | 32 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 33 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util" 34 | "google.golang.org/api/googleapi" 35 | sqladmin "google.golang.org/api/sqladmin/v1beta4" 36 | ) 37 | 38 | const defaultUserAgent = "custom cloud_sql_proxy version >= 1.10" 39 | 40 | // NewCertSource returns a CertSource which can be used to authenticate using 41 | // the provided client, which must not be nil. 42 | // 43 | // This function is deprecated; use NewCertSourceOpts instead. 44 | func NewCertSource(host string, c *http.Client, checkRegion bool) *RemoteCertSource { 45 | return NewCertSourceOpts(c, RemoteOpts{ 46 | APIBasePath: host, 47 | IgnoreRegion: !checkRegion, 48 | UserAgent: defaultUserAgent, 49 | }) 50 | } 51 | 52 | // RemoteOpts are a collection of options for NewCertSourceOpts. All fields are 53 | // optional. 54 | type RemoteOpts struct { 55 | // APIBasePath specifies the base path for the sqladmin API. If left blank, 56 | // the default from the autogenerated sqladmin library is used (which is 57 | // sufficient for nearly all users) 58 | APIBasePath string 59 | 60 | // IgnoreRegion specifies whether a missing or mismatched region in the 61 | // instance name should be ignored. In a future version this value will be 62 | // forced to 'false' by the RemoteCertSource. 63 | IgnoreRegion bool 64 | 65 | // A string for the RemoteCertSource to identify itself when contacting the 66 | // sqladmin API. 67 | UserAgent string 68 | 69 | // IP address type options 70 | IPAddrTypeOpts []string 71 | } 72 | 73 | // NewCertSourceOpts returns a CertSource configured with the provided Opts. 74 | // The provided http.Client must not be nil. 75 | // 76 | // Use this function instead of NewCertSource; it has a more forward-compatible 77 | // signature. 78 | func NewCertSourceOpts(c *http.Client, opts RemoteOpts) *RemoteCertSource { 79 | pkey, err := rsa.GenerateKey(rand.Reader, 2048) 80 | if err != nil { 81 | panic(err) // very unexpected. 82 | } 83 | serv, err := sqladmin.New(c) 84 | if err != nil { 85 | panic(err) // Only will happen if the provided client is nil. 86 | } 87 | if opts.APIBasePath != "" { 88 | serv.BasePath = opts.APIBasePath 89 | } 90 | ua := opts.UserAgent 91 | if ua == "" { 92 | ua = defaultUserAgent 93 | } 94 | serv.UserAgent = ua 95 | 96 | // Set default value to be "PUBLIC,PRIVATE" if not specified 97 | if len(opts.IPAddrTypeOpts) == 0 { 98 | opts.IPAddrTypeOpts = []string{"PUBLIC", "PRIVATE"} 99 | } 100 | 101 | // Add "PUBLIC" as an alias for "PRIMARY" 102 | for index, ipAddressType := range opts.IPAddrTypeOpts { 103 | if strings.ToUpper(ipAddressType) == "PUBLIC" { 104 | opts.IPAddrTypeOpts[index] = "PRIMARY" 105 | } 106 | } 107 | 108 | return &RemoteCertSource{pkey, serv, !opts.IgnoreRegion, opts.IPAddrTypeOpts} 109 | } 110 | 111 | // RemoteCertSource implements a CertSource, using Cloud SQL APIs to 112 | // return Local certificates for identifying oneself as a specific user 113 | // to the remote instance and Remote certificates for confirming the 114 | // remote database's identity. 115 | type RemoteCertSource struct { 116 | // key is the private key used for certificates returned by Local. 117 | key *rsa.PrivateKey 118 | // serv is used to make authenticated API calls to Cloud SQL. 119 | serv *sqladmin.Service 120 | // If set, providing an incorrect region in their connection string will be 121 | // treated as an error. This is to provide the same functionality that will 122 | // occur when API calls require the region. 123 | checkRegion bool 124 | // a list of ip address types that users select 125 | IPAddrTypes []string 126 | } 127 | 128 | // Constants for backoffAPIRetry. These cause the retry logic to scale the 129 | // backoff delay from 200ms to around 3.5s. 130 | const ( 131 | baseBackoff = float64(200 * time.Millisecond) 132 | backoffMult = 1.618 133 | backoffRetries = 5 134 | ) 135 | 136 | func backoffAPIRetry(desc, instance string, do func() error) error { 137 | var err error 138 | for i := 0; i < backoffRetries; i++ { 139 | err = do() 140 | gErr, ok := err.(*googleapi.Error) 141 | switch { 142 | case !ok: 143 | // 'ok' will also be false if err is nil. 144 | return err 145 | case gErr.Code == 403 && len(gErr.Errors) > 0 && gErr.Errors[0].Reason == "insufficientPermissions": 146 | // The case where the admin API has not yet been enabled. 147 | return fmt.Errorf("ensure that the Cloud SQL API is enabled for your project (https://console.cloud.google.com/flows/enableapi?apiid=sqladmin). Error during %s %s: %v", desc, instance, err) 148 | case gErr.Code == 404 || gErr.Code == 403: 149 | return fmt.Errorf("ensure that the account has access to %q (and make sure there's no typo in that name). Error during %s %s: %v", instance, desc, instance, err) 150 | case gErr.Code < 500: 151 | // Only Server-level HTTP errors are immediately retryable. 152 | return err 153 | } 154 | 155 | // sleep = baseBackoff * backoffMult^(retries + randomFactor) 156 | exp := float64(i+1) + mrand.Float64() 157 | sleep := time.Duration(baseBackoff * math.Pow(backoffMult, exp)) 158 | logging.Errorf("Error in %s %s: %v; retrying in %v", desc, instance, err, sleep) 159 | time.Sleep(sleep) 160 | } 161 | return err 162 | } 163 | 164 | // Local returns a certificate that may be used to establish a TLS 165 | // connection to the specified instance. 166 | func (s *RemoteCertSource) Local(instance string) (ret tls.Certificate, err error) { 167 | pkix, err := x509.MarshalPKIXPublicKey(&s.key.PublicKey) 168 | if err != nil { 169 | return ret, err 170 | } 171 | 172 | p, r, n := util.SplitName(instance) 173 | regionName := fmt.Sprintf("%s~%s", r, n) 174 | req := s.serv.SslCerts.CreateEphemeral(p, regionName, 175 | &sqladmin.SslCertsCreateEphemeralRequest{ 176 | PublicKey: string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"})), 177 | }, 178 | ) 179 | 180 | var data *sqladmin.SslCert 181 | err = backoffAPIRetry("createEphemeral for", instance, func() error { 182 | data, err = req.Do() 183 | return err 184 | }) 185 | if err != nil { 186 | return ret, err 187 | } 188 | 189 | c, err := parseCert(data.Cert) 190 | if err != nil { 191 | return ret, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err) 192 | } 193 | return tls.Certificate{ 194 | Certificate: [][]byte{c.Raw}, 195 | PrivateKey: s.key, 196 | Leaf: c, 197 | }, nil 198 | } 199 | 200 | func parseCert(pemCert string) (*x509.Certificate, error) { 201 | bl, _ := pem.Decode([]byte(pemCert)) 202 | if bl == nil { 203 | return nil, errors.New("invalid PEM: " + pemCert) 204 | } 205 | return x509.ParseCertificate(bl.Bytes) 206 | } 207 | 208 | // Find the first matching IP address by user input IP address types 209 | func (s *RemoteCertSource) findIPAddr(data *sqladmin.DatabaseInstance, instance string) (ipAddrInUse string, err error) { 210 | for _, eachIPAddrTypeByUser := range s.IPAddrTypes { 211 | for _, eachIPAddrTypeOfInstance := range data.IpAddresses { 212 | if strings.ToUpper(eachIPAddrTypeOfInstance.Type) == strings.ToUpper(eachIPAddrTypeByUser) { 213 | ipAddrInUse = eachIPAddrTypeOfInstance.IpAddress 214 | return ipAddrInUse, nil 215 | } 216 | } 217 | } 218 | 219 | ipAddrTypesOfInstance := "" 220 | for _, eachIPAddrTypeOfInstance := range data.IpAddresses { 221 | ipAddrTypesOfInstance += fmt.Sprintf("(TYPE=%v, IP_ADDR=%v)", eachIPAddrTypeOfInstance.Type, eachIPAddrTypeOfInstance.IpAddress) 222 | } 223 | 224 | ipAddrTypeOfUser := fmt.Sprintf("%v", s.IPAddrTypes) 225 | 226 | return "", fmt.Errorf("User input IP address type %v does not match the instance %v, the instance's IP addresses are %v ", ipAddrTypeOfUser, instance, ipAddrTypesOfInstance) 227 | } 228 | 229 | // Remote returns the specified instance's CA certificate, address, and name. 230 | func (s *RemoteCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) { 231 | p, region, n := util.SplitName(instance) 232 | regionName := fmt.Sprintf("%s~%s", region, n) 233 | req := s.serv.Instances.Get(p, regionName) 234 | 235 | var data *sqladmin.DatabaseInstance 236 | err = backoffAPIRetry("get instance", instance, func() error { 237 | data, err = req.Do() 238 | return err 239 | }) 240 | if err != nil { 241 | return nil, "", "", "", err 242 | } 243 | 244 | // TODO(chowski): remove this when us-central is removed. 245 | if data.Region == "us-central" { 246 | data.Region = "us-central1" 247 | } 248 | if data.Region != region { 249 | if region == "" { 250 | err = fmt.Errorf("instance %v doesn't provide region", instance) 251 | } else { 252 | err = fmt.Errorf(`for connection string "%s": got region %q, want %q`, instance, region, data.Region) 253 | } 254 | if s.checkRegion { 255 | return nil, "", "", "", err 256 | } 257 | logging.Errorf("%v", err) 258 | logging.Errorf("WARNING: specifying the correct region in an instance string will become required in a future version!") 259 | } 260 | 261 | if len(data.IpAddresses) == 0 { 262 | return nil, "", "", "", fmt.Errorf("no IP address found for %v", instance) 263 | } 264 | if data.BackendType == "FIRST_GEN" { 265 | logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") 266 | return nil, "", "", "", fmt.Errorf("%q is a first generation instance", instance) 267 | } 268 | 269 | // Find the first matching IP address by user input IP address types 270 | ipAddrInUse := "" 271 | ipAddrInUse, err = s.findIPAddr(data, instance) 272 | if err != nil { 273 | return nil, "", "", "", err 274 | } 275 | 276 | c, err := parseCert(data.ServerCaCert.Cert) 277 | 278 | return c, ipAddrInUse, p + ":" + n, data.DatabaseVersion, err 279 | } 280 | -------------------------------------------------------------------------------- /examples/kubernetes/README.md: -------------------------------------------------------------------------------- 1 | # Using the Cloud SQL proxy on Kubernetes 2 | 3 | The Cloud SQL proxy is the recommended way to connect to Cloud SQL, even when 4 | using private IP. This is because the proxy provides strong encryption and 5 | authentication using IAM, which help keep your database secure. 6 | 7 | ## Configure your application with Secrets 8 | 9 | In Kubernetes, [Secrets][ksa-secret] are a secure way to pass configuration 10 | details to your application. Each Secret object can contain multiple key/value 11 | pairs that can be pass to your application in multiple ways. When connecting to 12 | a database, you can create a Secret with details such as your database name, 13 | user, and password which can be injected into your application as env vars. 14 | 15 | 1. Create a secret with information needed to access your database: 16 | ```shell 17 | kubectl create secret generic \ 18 | --from-literal=username= \ 19 | --from-literal=password= \ 20 | --from-literal=database= 21 | ``` 22 | 2. Next, configure your application's container to mount the secrets as env 23 | vars: 24 | > [proxy_with_workload_identity.yaml](proxy_with_workload_identity.yaml#L12-L30) 25 | ```yaml 26 | env: 27 | - name: DB_USER 28 | valueFrom: 29 | secretKeyRef: 30 | name: 31 | key: username 32 | - name: DB_PASS 33 | valueFrom: 34 | secretKeyRef: 35 | name: 36 | key: password 37 | - name: DB_NAME 38 | valueFrom: 39 | secretKeyRef: 40 | name: 41 | key: database 42 | ``` 43 | 3. Finally, configure your application to use these values. In the example 44 | above, the values will be in the env vars `DB_USER`, `DB_PASS`, and `DB_NAME`. 45 | 46 | [ksa-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ 47 | 48 | ## Setting up a service account 49 | 50 | The first step to running the Cloud SQL proxy in Kubernetes is creating a 51 | service account to represent your application. It is recommended that you create 52 | a service account unique to each application, instead of using the same service 53 | account everywhere. This model is more secure since it allows your to limit 54 | permissions on a per-application basis. 55 | 56 | The service account for your application needs to meet the following criteria: 57 | 58 | 1. Belong to a project with the [Cloud SQL Admin API][admin-api] enabled 59 | 1. [Has been granted][grant-sa] the 60 | [`Cloud SQL Client` IAM role (or equivalent)][csql-roles] 61 | for the project containing the instance you want to connect to 62 | 1. If connecting using private IP, you must use a 63 | [VPC-native GKE cluster][vpc-gke], in the same VPC as your Cloud SQL instance 64 | 65 | [admin-api]: https://console.cloud.google.com/flows/enableapi?apiid=sqladmin&redirect=https://console.cloud.google.com 66 | [grant-sa]: https://cloud.google.com/iam/docs/granting-roles-to-service-accounts 67 | [csql-roles]: https://cloud.google.com/iam/docs/understanding-roles#cloud-sql-roles 68 | [vpc-gke]: https://cloud.google.com/kubernetes-engine/docs/how-to/alias-ips 69 | 70 | ## Providing the service account to the proxy 71 | 72 | Next, you need to configure Kubernetes to provide the service account to the 73 | Cloud SQL Proxy. There are two recommended ways to do this. 74 | 75 | ### Workload Identity 76 | 77 | If you are using [Google Kubernetes Engine][gke], the preferred method is to 78 | use GKE's [Workload Identity][workload-id] feature. This method allows you to 79 | bind a [Kubernetes Service Account (KSA)][ksa] to a Google Service Account 80 | (GSA). The GSA will then be accessible to applications using the matching KSA. 81 | 82 | 1. [Enable Workload Identity for your cluster][enable-wi] 83 | 1. Create a KSA for your application `kubectl apply -f service-account.yaml`: 84 | 85 | > [service-account.yaml](service-account.yaml#L2-L5) 86 | ```yaml 87 | apiVersion: v1 88 | kind: ServiceAccount 89 | metadata: 90 | name: # TODO(developer): replace these values 91 | ``` 92 | 1. Enable the IAM binding between your `` and ``: 93 | ```sh 94 | gcloud iam service-accounts add-iam-policy-binding \ 95 | --role roles/iam.workloadIdentityUser \ 96 | --member "serviceAccount:.svc.id.goog[/]" \ 97 | @.iam.gserviceaccount.com 98 | ``` 99 | 1. Add an annotation to `` to complete the binding: 100 | ```sh 101 | kubectl annotate serviceaccount \ 102 | \ 103 | iam.gke.io/gcp-service-account=@.iam.gserviceaccount.com 104 | ``` 105 | 1. Finally, make sure to specify the service account for the k8s pod spec: 106 | > [proxy_with_workload_identity.yaml](proxy_with_workload_identity.yaml#L2-L15) 107 | ```yaml 108 | apiVersion: apps/v1 109 | kind: Deployment 110 | metadata: 111 | name: 112 | spec: 113 | selector: 114 | matchLabels: 115 | app: 116 | template: 117 | metadata: 118 | labels: 119 | app: 120 | spec: 121 | serviceAccountName: 122 | ``` 123 | 124 | [gke]: https://cloud.google.com/kubernetes-engine 125 | [workload-id]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity 126 | [ksa]: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ 127 | [enable-wi]: https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable_on_existing_cluster 128 | 129 | 130 | ### Service account key file 131 | 132 | Alternatively, if your can't use Workload Identity, the recommended pattern is 133 | to mount a service account key file into the Cloud SQL proxy pod and use the 134 | `-credential_file` flag. 135 | 136 | 1. Create a credential file for your service account key: 137 | ```sh 138 | gcloud iam service-accounts keys create ~key.json \ 139 | --iam-account >@project-id.iam.gserviceaccount.com 140 | ``` 141 | 1. Turn your service account key into a k8s [Secret][k8s-secret]: 142 | ```shell 143 | kubectl create secret generic \ 144 | --from-file=service_account.json=~/key.json 145 | ``` 146 | 3. Mount the secret as a volume under the`spec:` for your k8s object: 147 | > [proxy_with_sa_key.yaml](proxy_with_sa_key.yaml#L55-L58) 148 | ```yaml 149 | volumes: 150 | - name: 151 | secret: 152 | secretName: 153 | ``` 154 | 155 | 4. Follow the instructions in the next section to access the volume from the 156 | proxy's pod. 157 | 158 | [k8s-secret]: https://kubernetes.io/docs/concepts/configuration/secret/ 159 | 160 | ## Run the Cloud SQL proxy as a sidecar 161 | 162 | We recommend running the proxy in a "sidecar" pattern (as an additional 163 | container sharing a pod with your application). We recommend this over running 164 | as a separate service for several reasons: 165 | 166 | * Prevents your SQL traffic from being exposed locally - the proxy provides 167 | encryption on outgoing connections, but you should limit exposure for 168 | incoming connections 169 | * Prevents a single point of failure - each application's access to 170 | your database is independent from the others, making it more resilient. 171 | * Limits access to the proxy, allowing you to use IAM permissions per 172 | application rather than exposing the database to the entire cluster 173 | * Allows you to scope resource requests more accurately - because the 174 | proxy consumes resources linearly to usage, this pattern allows you to more 175 | accurately scope and request resources to match your applications as it 176 | scales 177 | 178 | 1. Add the Cloud SQL proxy to the pod configuration under `containers:` : 179 | > [proxy_with_workload-identity.yaml](proxy_with_workload_identity.yaml#L33-L549) 180 | ```yaml 181 | - name: cloud-sql-proxy 182 | # It is recommended to use the latest version of the Cloud SQL proxy 183 | # Make sure to update on a regular schedule! 184 | image: gcr.io/cloudsql-docker/gce-proxy:1.17 185 | command: 186 | - "/cloud_sql_proxy" 187 | 188 | # If connecting from a VPC-native GKE cluster, you can use the 189 | # following flag to have the proxy connect over private IP 190 | # - "-ip_address_types=PRIVATE" 191 | 192 | # Replace DB_PORT with the port the proxy should listen on 193 | # Defaults: MySQL: 3306, Postgres: 5432, SQLServer: 1433 194 | - "-instances==tcp:" 195 | securityContext: 196 | # The default Cloud SQL proxy image runs as the 197 | # "nonroot" user and group (uid: 65532) by default. 198 | runAsNonRoot: true 199 | ``` 200 | If you are using a service account key, specify your secret volume and add 201 | the `-credential_file` flag to the command: 202 | 203 | > [proxy_with_sa_key.yaml](proxy_with_sa_key.yaml#L43-L52) 204 | ```yaml 205 | # This flag specifies where the service account key can be found 206 | - "-credential_file=/secrets/service_account.json" 207 | securityContext: 208 | # The default Cloud SQL proxy image runs as the 209 | # "nonroot" user and group (uid: 65532) by default. 210 | runAsNonRoot: true 211 | volumeMounts: 212 | - name: 213 | mountPath: /secrets/ 214 | readOnly: true 215 | ``` 216 | 217 | 1. Finally, configure your application to connect via `127.0.0.1` on whichever 218 | `` you specified in the command section. 219 | 220 | 221 | ## Connecting without the Cloud SQL proxy 222 | 223 | While not as secure, it is possible to connect from a VPC-native GKE cluster to 224 | a Cloud SQL instance on the same VPC using private IP without the proxy. 225 | 226 | 1. Create a secret with your instance's private IP address: 227 | ```shell 228 | kubectl create secret generic \ 229 | --from-literal=db_host= 230 | ``` 231 | 232 | 2. Next make sure you add the secret to your application's container: 233 | > [no_proxy_private_ip.yaml](no_proxy_private_ip.yaml#L28-L32) 234 | ```yaml 235 | - name: DB_HOST 236 | valueFrom: 237 | secretKeyRef: 238 | name: 239 | key: db_host 240 | ``` 241 | 242 | 3. Finally, configure your application to connect using the IP address from the 243 | `DB_HOST` env var. You will need to use the correct port for your db-engine 244 | (MySQL: `3306`, Postgres: `5432`, SQLServer: `1433`). 245 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /proxy/fuse/fuse.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // +build !windows,!openbsd 16 | 17 | // Package fuse provides a connection source wherein the user does not need to 18 | // specify which instance they are connecting to before they start the 19 | // executable. Instead, simply attempting to access a file in the provided 20 | // directory will transparently create a proxied connection to an instance 21 | // which has that name. 22 | // 23 | // Specifically, given that NewConnSrc was called with the mounting directory 24 | // as /cloudsql: 25 | // 26 | // 1) Execute `mysql -S /cloudsql/speckle:instance` 27 | // 2) The 'mysql' executable looks up the file "speckle:instance" inside "/cloudsql" 28 | // 3) This lookup is intercepted by the code in this package. A local unix socket 29 | // located in a temporary directory is opened for listening and the lookup for 30 | // "speckle:instance" returns to mysql saying that it is a symbolic link 31 | // pointing to this new local socket. 32 | // 4) mysql dials the local unix socket, creating a new connection to the 33 | // specified instance. 34 | package fuse 35 | 36 | import ( 37 | "bytes" 38 | "errors" 39 | "fmt" 40 | "io" 41 | "net" 42 | "os" 43 | "path/filepath" 44 | "strings" 45 | "sync" 46 | "time" 47 | 48 | "bazil.org/fuse" 49 | "bazil.org/fuse/fs" 50 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 51 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" 52 | "golang.org/x/net/context" 53 | ) 54 | 55 | // Supported returns true if the current system supports FUSE. 56 | // TODO: for OSX, check to see if OSX FUSE is installed. 57 | func Supported() bool { 58 | return true 59 | } 60 | 61 | // NewConnSrc returns a source of new connections based on Lookups in the 62 | // provided mount directory. If there isn't a directory located at tmpdir one 63 | // is created. The second return parameter can be used to shutdown and release 64 | // any resources. As a result of this shutdown, or during any other fatal 65 | // error, the returned chan will be closed. 66 | // 67 | // The connset parameter is optional. 68 | func NewConnSrc(mountdir, tmpdir string, client *proxy.Client, connset *proxy.ConnSet) (<-chan proxy.Conn, io.Closer, error) { 69 | if err := os.MkdirAll(tmpdir, 0777); err != nil { 70 | return nil, nil, err 71 | } 72 | 73 | logging.Verbosef("Mounting %v...", mountdir) 74 | c, err := fuse.Mount(mountdir, fuse.AllowOther()) 75 | if err != nil { 76 | // a common cause of failed mounts is that a previous instance did not shutdown cleanly, leaving an abandoned mount 77 | logging.Errorf("WARNING: Mount failed - attempting to unmount dir to resolve...", mountdir) 78 | if err = fuse.Unmount(mountdir); err != nil { 79 | logging.Errorf("Unmount failed: %v", err) 80 | } 81 | if c, err = fuse.Mount(mountdir, fuse.AllowOther()); err != nil { 82 | return nil, nil, fmt.Errorf("cannot mount %q: %v", mountdir, err) 83 | } 84 | } 85 | logging.Infof("Mounted %v", mountdir) 86 | 87 | if connset == nil { 88 | // Make a dummy one. 89 | connset = proxy.NewConnSet() 90 | } 91 | conns := make(chan proxy.Conn, 1) 92 | root := &fsRoot{ 93 | tmpDir: tmpdir, 94 | linkDir: mountdir, 95 | dst: conns, 96 | links: make(map[string]symlink), 97 | closers: []io.Closer{c}, 98 | connset: connset, 99 | client: client, 100 | } 101 | 102 | server := fs.New(c, &fs.Config{ 103 | Debug: func(msg interface{}) { 104 | if false { 105 | logging.Verbosef("%s", msg) 106 | } 107 | }, 108 | }) 109 | 110 | go func() { 111 | if err := server.Serve(root); err != nil { 112 | logging.Errorf("serve %q exited due to error: %v", mountdir, err) 113 | } 114 | // The server exited but we don't know whether this is because of a 115 | // graceful reason (via root.Close) or via an external force unmounting. 116 | // Closing the root will ensure the 'dst' chan is closed correctly to 117 | // signify that no new connections are possible. 118 | if err := root.Close(); err != nil { 119 | logging.Errorf("root.Close() error: %v", err) 120 | } 121 | logging.Infof("FUSE exited") 122 | }() 123 | 124 | return conns, root, nil 125 | } 126 | 127 | // symlink implements a symbolic link, returning the underlying string when 128 | // Readlink is called. 129 | type symlink string 130 | 131 | var _ interface { 132 | fs.Node 133 | fs.NodeReadlinker 134 | } = symlink("") 135 | 136 | func (s symlink) Readlink(context.Context, *fuse.ReadlinkRequest) (string, error) { 137 | return string(s), nil 138 | } 139 | 140 | // Attr helps implement fs.Node. 141 | func (symlink) Attr(ctx context.Context, a *fuse.Attr) error { 142 | *a = fuse.Attr{ 143 | Mode: 0777 | os.ModeSymlink, 144 | } 145 | return nil 146 | } 147 | 148 | type fsRoot struct { 149 | tmpDir, linkDir string 150 | 151 | client *proxy.Client 152 | connset *proxy.ConnSet 153 | 154 | // sockLock protects fields in this struct related to sockets; specifically 155 | // 'links' and 'closers'. 156 | sockLock sync.Mutex 157 | links map[string]symlink 158 | // closers holds a slice of things to close when fsRoot.Close is called. 159 | closers []io.Closer 160 | 161 | sync.RWMutex 162 | dst chan<- proxy.Conn 163 | } 164 | 165 | // Ensure that fsRoot implements the following interfaces. 166 | var _ interface { 167 | fs.FS 168 | fs.Node 169 | fs.NodeRequestLookuper 170 | fs.HandleReadDirAller 171 | } = &fsRoot{} 172 | 173 | func (r *fsRoot) newConn(instance string, c net.Conn) { 174 | r.RLock() 175 | // dst will be nil if Close has been called already. 176 | if ch := r.dst; ch != nil { 177 | ch <- proxy.Conn{instance, c} 178 | } else { 179 | logging.Errorf("Ignored new conn request to %q: system has been closed", instance) 180 | } 181 | r.RUnlock() 182 | } 183 | 184 | func (r *fsRoot) Forget() { 185 | logging.Verbosef("Forget called on %q", r.linkDir) 186 | } 187 | 188 | func (r *fsRoot) Destroy() { 189 | logging.Verbosef("Destroy called on %q", r.linkDir) 190 | } 191 | 192 | func (r *fsRoot) Close() error { 193 | r.Lock() 194 | if r.dst != nil { 195 | // Since newConn only sends on dst while holding a reader lock, holding the 196 | // writer lock is sufficient to ensure there are no pending sends on the 197 | // channel when it is closed. 198 | close(r.dst) 199 | // Setting it to nil prevents further sends. 200 | r.dst = nil 201 | } 202 | r.Unlock() 203 | 204 | logging.Infof("unmount %q", r.linkDir) 205 | if err := fuse.Unmount(r.linkDir); err != nil { 206 | return err 207 | } 208 | 209 | var errs bytes.Buffer 210 | r.sockLock.Lock() 211 | for _, c := range r.closers { 212 | if err := c.Close(); err != nil { 213 | fmt.Fprintln(&errs, err) 214 | } 215 | } 216 | r.sockLock.Unlock() 217 | 218 | if errs.Len() == 0 { 219 | return nil 220 | } 221 | logging.Errorf("Close %q: %v", r.linkDir, errs.String()) 222 | return errors.New(errs.String()) 223 | } 224 | 225 | // Root returns the fsRoot itself as the root directory. 226 | func (r *fsRoot) Root() (fs.Node, error) { 227 | return r, nil 228 | } 229 | 230 | // Attr helps implement fs.Node 231 | func (r *fsRoot) Attr(ctx context.Context, a *fuse.Attr) error { 232 | *a = fuse.Attr{ 233 | Mode: 0555 | os.ModeDir, 234 | } 235 | return nil 236 | } 237 | 238 | // Lookup helps implement fs.NodeRequestLookuper. If the requested file isn't 239 | // the README, it returns a node which is a symbolic link to a socket which 240 | // provides connectivity to a remote instance. The instance which is connected 241 | // to is determined by req.Name. 242 | func (r *fsRoot) Lookup(_ context.Context, req *fuse.LookupRequest, resp *fuse.LookupResponse) (fs.Node, error) { 243 | if req.Name == "README" { 244 | return readme{}, nil 245 | } 246 | instance := req.Name 247 | r.sockLock.Lock() 248 | defer r.sockLock.Unlock() 249 | 250 | if ret, ok := r.links[instance]; ok { 251 | return ret, nil 252 | } 253 | 254 | path := filepath.Join(r.tmpDir, instance) 255 | os.RemoveAll(path) // Best effort; the following will fail if this does. 256 | linkpath := path 257 | 258 | // MySQL expects a Unix domain socket at the symlinked path whereas Postgres expects 259 | // a socket named ".s.PGSQL.5432" in the directory given by the database path. 260 | // Look up instance database version to determine the correct socket path. 261 | // Client is nil in unit tests. 262 | if r.client != nil { 263 | version, err := r.client.InstanceVersion(instance) 264 | if err != nil { 265 | return nil, fuse.ENOENT 266 | } 267 | if strings.HasPrefix(strings.ToLower(version), "postgres") { 268 | if err := os.MkdirAll(path, 0755); err != nil { 269 | return nil, fuse.EIO 270 | } 271 | path = filepath.Join(linkpath, ".s.PGSQL.5432") 272 | } 273 | } 274 | 275 | sock, err := net.Listen("unix", path) 276 | if err != nil { 277 | logging.Errorf("couldn't listen at %q: %v", path, err) 278 | return nil, fuse.EEXIST 279 | } 280 | if err := os.Chmod(path, 0777|os.ModeSocket); err != nil { 281 | logging.Errorf("couldn't update permissions for socket file %q: %v; other users may be unable to connect", path, err) 282 | } 283 | 284 | go r.listenerLifecycle(sock, instance, path) 285 | 286 | ret := symlink(linkpath) 287 | r.links[instance] = ret 288 | // TODO(chowski): memory leak when listeners exit on their own via removeListener. 289 | r.closers = append(r.closers, sock) 290 | 291 | return ret, nil 292 | } 293 | 294 | // removeListener marks that a Listener for an instance has exited and is no 295 | // longer serving new connections. 296 | func (r *fsRoot) removeListener(instance, path string) { 297 | r.sockLock.Lock() 298 | v, ok := r.links[instance] 299 | if ok && string(v) == path { 300 | delete(r.links, instance) 301 | } else { 302 | logging.Errorf("Removing a listener for %q at %q which was already replaced", instance, path) 303 | } 304 | r.sockLock.Unlock() 305 | } 306 | 307 | // listenerLifecycle calls l.Accept in a loop, and for each new connection 308 | // r.newConn is called. After the Listener returns an error it is removed. 309 | func (r *fsRoot) listenerLifecycle(l net.Listener, instance, path string) { 310 | for { 311 | start := time.Now() 312 | c, err := l.Accept() 313 | if err != nil { 314 | logging.Errorf("error in Accept for %q: %v", instance, err) 315 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() { 316 | d := 10*time.Millisecond - time.Since(start) 317 | if d > 0 { 318 | time.Sleep(d) 319 | } 320 | continue 321 | } 322 | break 323 | } 324 | r.newConn(instance, c) 325 | } 326 | r.removeListener(instance, path) 327 | l.Close() 328 | if err := os.Remove(path); err != nil { 329 | logging.Errorf("couldn't remove %q: %v", path, err) 330 | } 331 | } 332 | 333 | // ReadDirAll returns a list of files contained in the root directory. 334 | // It contains a README file which explains how to use the directory. 335 | // In addition, there will be a file for each instance to which the 336 | // proxy is actively connected. 337 | func (r *fsRoot) ReadDirAll(context.Context) ([]fuse.Dirent, error) { 338 | ret := []fuse.Dirent{ 339 | {Name: "README", Type: fuse.DT_File}, 340 | } 341 | 342 | for _, v := range r.connset.IDs() { 343 | ret = append(ret, fuse.Dirent{Name: v, Type: fuse.DT_Socket}) 344 | } 345 | 346 | return ret, nil 347 | } 348 | 349 | // readme implements the REAME file found in the mounted folder. It is a 350 | // static read-only text file. 351 | type readme struct{} 352 | 353 | var _ interface { 354 | fs.Node 355 | fs.HandleReadAller 356 | } = readme{} 357 | 358 | const readmeText = ` 359 | When programs attempt to open files in this directory, a remote connection to 360 | the Cloud SQL instance of the same name will be established. 361 | 362 | That is, running : 363 | 364 | mysql -u root -S "/path/to/this/directory/project:instance-2" 365 | 366 | will open a new connection to project:instance-2, given you have the correct 367 | permissions. 368 | 369 | Listing the contents of this directory will show all instances with active 370 | connections. 371 | ` 372 | 373 | // Attr helps implement fs.Node. 374 | func (readme) Attr(ctx context.Context, a *fuse.Attr) error { 375 | *a = fuse.Attr{ 376 | Mode: 0444, 377 | Size: uint64(len(readmeText)), 378 | } 379 | return nil 380 | } 381 | 382 | // ReadAll helps implement fs.HandleReadAller. 383 | func (readme) ReadAll(context.Context) ([]byte, error) { 384 | return []byte(readmeText), nil 385 | } 386 | -------------------------------------------------------------------------------- /cmd/cloud_sql_proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | // This file contains code for supporting local sockets for the Cloud SQL Proxy. 18 | 19 | import ( 20 | "bytes" 21 | "errors" 22 | "fmt" 23 | "net" 24 | "net/http" 25 | "os" 26 | "path/filepath" 27 | "runtime" 28 | "strings" 29 | "time" 30 | 31 | "github.com/GoogleCloudPlatform/cloudsql-proxy/logging" 32 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/fuse" 33 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/proxy" 34 | "github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util" 35 | sqladmin "google.golang.org/api/sqladmin/v1beta4" 36 | ) 37 | 38 | // WatchInstances handles the lifecycle of local sockets used for proxying 39 | // local connections. Values received from the updates channel are 40 | // interpretted as a comma-separated list of instances. The set of sockets in 41 | // 'dir' is the union of 'instances' and the most recent list from 'updates'. 42 | func WatchInstances(dir string, cfgs []instanceConfig, updates <-chan string, cl *http.Client) (<-chan proxy.Conn, error) { 43 | ch := make(chan proxy.Conn, 1) 44 | 45 | // Instances specified statically (e.g. as flags to the binary) will always 46 | // be available. They are ignored if also returned by the GCE metadata since 47 | // the socket will already be open. 48 | staticInstances := make(map[string]net.Listener, len(cfgs)) 49 | for _, v := range cfgs { 50 | l, err := listenInstance(ch, v) 51 | if err != nil { 52 | return nil, err 53 | } 54 | staticInstances[v.Instance] = l 55 | } 56 | 57 | if updates != nil { 58 | go watchInstancesLoop(dir, ch, updates, staticInstances, cl) 59 | } 60 | return ch, nil 61 | } 62 | 63 | func watchInstancesLoop(dir string, dst chan<- proxy.Conn, updates <-chan string, static map[string]net.Listener, cl *http.Client) { 64 | dynamicInstances := make(map[string]net.Listener) 65 | for instances := range updates { 66 | // All instances were legal when we started, so we pass false below to ensure we don't skip them 67 | // later if they became unhealthy for some reason; this would be a serious enough problem. 68 | list, err := parseInstanceConfigs(dir, strings.Split(instances, ","), cl, false) 69 | if err != nil { 70 | logging.Errorf("%v", err) 71 | // If we do not have a valid list of instances, skip this update 72 | continue 73 | } 74 | 75 | stillOpen := make(map[string]net.Listener) 76 | for _, cfg := range list { 77 | instance := cfg.Instance 78 | 79 | // If the instance is specified in the static list don't do anything: 80 | // it's already open and should stay open forever. 81 | if _, ok := static[instance]; ok { 82 | continue 83 | } 84 | 85 | if l, ok := dynamicInstances[instance]; ok { 86 | delete(dynamicInstances, instance) 87 | stillOpen[instance] = l 88 | continue 89 | } 90 | 91 | l, err := listenInstance(dst, cfg) 92 | if err != nil { 93 | logging.Errorf("Couldn't open socket for %q: %v", instance, err) 94 | continue 95 | } 96 | stillOpen[instance] = l 97 | } 98 | 99 | // Any instance in dynamicInstances was not in the most recent metadata 100 | // update. Clean up those instances' sockets by closing them; note that 101 | // this does not affect any existing connections instance. 102 | for instance, listener := range dynamicInstances { 103 | logging.Infof("Closing socket for instance %v", instance) 104 | listener.Close() 105 | } 106 | 107 | dynamicInstances = stillOpen 108 | } 109 | 110 | for _, v := range static { 111 | if err := v.Close(); err != nil { 112 | logging.Errorf("Error closing %q: %v", v.Addr(), err) 113 | } 114 | } 115 | for _, v := range dynamicInstances { 116 | if err := v.Close(); err != nil { 117 | logging.Errorf("Error closing %q: %v", v.Addr(), err) 118 | } 119 | } 120 | } 121 | 122 | func remove(path string) { 123 | if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 124 | logging.Infof("Remove(%q) error: %v", path, err) 125 | } 126 | } 127 | 128 | // listenInstance starts listening on a new unix socket in dir to connect to the 129 | // specified instance. New connections to this socket are sent to dst. 130 | func listenInstance(dst chan<- proxy.Conn, cfg instanceConfig) (net.Listener, error) { 131 | unix := cfg.Network == "unix" 132 | if unix { 133 | remove(cfg.Address) 134 | } 135 | l, err := net.Listen(cfg.Network, cfg.Address) 136 | if err != nil { 137 | return nil, err 138 | } 139 | if unix { 140 | if err := os.Chmod(cfg.Address, 0777|os.ModeSocket); err != nil { 141 | logging.Errorf("couldn't update permissions for socket file %q: %v; other users may not be unable to connect", cfg.Address, err) 142 | } 143 | } 144 | 145 | go func() { 146 | for { 147 | start := time.Now() 148 | c, err := l.Accept() 149 | if err != nil { 150 | logging.Errorf("Error in accept for %q on %v: %v", cfg, cfg.Address, err) 151 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() { 152 | d := 10*time.Millisecond - time.Since(start) 153 | if d > 0 { 154 | time.Sleep(d) 155 | } 156 | continue 157 | } 158 | l.Close() 159 | return 160 | } 161 | logging.Verbosef("New connection for %q", cfg.Instance) 162 | 163 | switch clientConn := c.(type) { 164 | case *net.TCPConn: 165 | clientConn.SetKeepAlive(true) 166 | clientConn.SetKeepAlivePeriod(1 * time.Minute) 167 | 168 | } 169 | dst <- proxy.Conn{cfg.Instance, c} 170 | } 171 | }() 172 | 173 | logging.Infof("Listening on %s for %s", cfg.Address, cfg.Instance) 174 | return l, nil 175 | } 176 | 177 | type instanceConfig struct { 178 | Instance string 179 | Network, Address string 180 | } 181 | 182 | // loopbackForNet maps a network (e.g. tcp6) to the loopback address for that 183 | // network. It is updated during the initialization of validNets to include a 184 | // valid loopback address for "tcp". 185 | var loopbackForNet = map[string]string{ 186 | "tcp4": "127.0.0.1", 187 | "tcp6": "::1", 188 | } 189 | 190 | // validNets tracks the networks that are valid for this platform and machine. 191 | var validNets = func() map[string]bool { 192 | m := map[string]bool{ 193 | "unix": runtime.GOOS != "windows", 194 | } 195 | 196 | anyTCP := false 197 | for _, n := range []string{"tcp4", "tcp6"} { 198 | host, ok := loopbackForNet[n] 199 | if !ok { 200 | // This is effectively a compile-time error. 201 | panic(fmt.Sprintf("no loopback address found for %v", n)) 202 | } 203 | // Open any port to see if the net is valid. 204 | x, err := net.Listen(n, net.JoinHostPort(host, "0")) 205 | if err != nil { 206 | // Error is too verbose to be useful. 207 | continue 208 | } 209 | x.Close() 210 | m[n] = true 211 | 212 | if !anyTCP { 213 | anyTCP = true 214 | // Set the loopback value for generic tcp if it hasn't already been 215 | // set. (If both tcp4/tcp6 are supported the first one in the list 216 | // (tcp4's 127.0.0.1) is used. 217 | loopbackForNet["tcp"] = host 218 | } 219 | } 220 | if anyTCP { 221 | m["tcp"] = true 222 | } 223 | return m 224 | }() 225 | 226 | func parseInstanceConfig(dir, instance string, cl *http.Client) (instanceConfig, error) { 227 | var ret instanceConfig 228 | args := strings.Split(instance, "=") 229 | if len(args) > 2 { 230 | return instanceConfig{}, fmt.Errorf("invalid instance argument: must be either form - `` or `=`; invalid arg was %q", instance) 231 | } 232 | // Parse the instance connection name - everything before the "=". 233 | ret.Instance = args[0] 234 | proj, region, name := util.SplitName(ret.Instance) 235 | regionName := fmt.Sprintf("%s~%s", region, name) 236 | if proj == "" || region == "" || name == "" { 237 | return instanceConfig{}, fmt.Errorf("invalid instance connection string: must be in the form `project:region:instance-name`; invalid name was %q", args[0]) 238 | } 239 | if len(args) == 1 { 240 | // Default to listening via unix socket in specified directory 241 | ret.Network = "unix" 242 | ret.Address = filepath.Join(dir, instance) 243 | } else { 244 | // Parse the instance options if present. 245 | opts := strings.SplitN(args[1], ":", 2) 246 | if len(opts) != 2 { 247 | return instanceConfig{}, fmt.Errorf("invalid instance options: must be in the form `unix:/path/to/socket`, `tcp:port`, `tcp:host:port`; invalid option was %q", strings.Join(opts, ":")) 248 | } 249 | ret.Network = opts[0] 250 | var err error 251 | if ret.Network == "unix" { 252 | if strings.HasPrefix(opts[1], "/") { 253 | ret.Address = opts[1] // Root path. 254 | } else { 255 | ret.Address = filepath.Join(dir, opts[1]) 256 | } 257 | } else { 258 | ret.Address, err = parseTCPOpts(opts[0], opts[1]) 259 | } 260 | if err != nil { 261 | return instanceConfig{}, err 262 | } 263 | } 264 | 265 | // Use the SQL Admin API to verify compatibility with the instance. 266 | sql, err := sqladmin.New(cl) 267 | if err != nil { 268 | return instanceConfig{}, err 269 | } 270 | if *host != "" { 271 | sql.BasePath = *host 272 | } 273 | inst, err := sql.Instances.Get(proj, regionName).Do() 274 | if err != nil { 275 | return instanceConfig{}, err 276 | } 277 | if inst.BackendType == "FIRST_GEN" { 278 | logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.") 279 | return instanceConfig{}, fmt.Errorf("%q is a first generation instance", instance) 280 | } 281 | // Postgres instances use a special suffix on the unix socket. 282 | // See https://www.postgresql.org/docs/11/runtime-config-connection.html 283 | if ret.Network == "unix" && strings.HasPrefix(strings.ToLower(inst.DatabaseVersion), "postgres") { 284 | // Verify the directory exists. 285 | if err := os.MkdirAll(ret.Address, 0755); err != nil { 286 | return instanceConfig{}, err 287 | } 288 | ret.Address = filepath.Join(ret.Address, ".s.PGSQL.5432") 289 | } 290 | 291 | if !validNets[ret.Network] { 292 | return ret, fmt.Errorf("invalid %q: unsupported network: %v", instance, ret.Network) 293 | } 294 | return ret, nil 295 | } 296 | 297 | // parseTCPOpts parses the instance options when specifying tcp port options. 298 | func parseTCPOpts(ntwk, addrOpt string) (string, error) { 299 | if strings.Contains(addrOpt, ":") { 300 | return addrOpt, nil // User provided a host and port; use that. 301 | } 302 | // No "host" part of the address. Be safe and assume that they want a loopback address. 303 | addr, ok := loopbackForNet[ntwk] 304 | if !ok { 305 | return "", fmt.Errorf("invalid %q:%q: unrecognized network %v", ntwk, addrOpt, ntwk) 306 | } 307 | return net.JoinHostPort(addr, addrOpt), nil 308 | } 309 | 310 | // parseInstanceConfigs calls parseInstanceConfig for each instance in the 311 | // provided slice, collecting errors along the way. There may be valid 312 | // instanceConfigs returned even if there's an error. 313 | func parseInstanceConfigs(dir string, instances []string, cl *http.Client, skipFailedInstanceConfigs bool) ([]instanceConfig, error) { 314 | errs := new(bytes.Buffer) 315 | var cfg []instanceConfig 316 | for _, v := range instances { 317 | if v == "" { 318 | continue 319 | } 320 | if c, err := parseInstanceConfig(dir, v, cl); err != nil { 321 | if skipFailedInstanceConfigs { 322 | logging.Infof("There was a problem when parsing a instance configuration but ignoring due to the configuration. Error: %v", err) 323 | } else { 324 | fmt.Fprintf(errs, "\n\t%v", err) 325 | } 326 | 327 | } else { 328 | cfg = append(cfg, c) 329 | } 330 | } 331 | 332 | var err error 333 | if errs.Len() > 0 { 334 | err = fmt.Errorf("errors parsing config:%s", errs) 335 | } 336 | return cfg, err 337 | } 338 | 339 | // CreateInstanceConfigs verifies that the parameters passed to it are valid 340 | // for the proxy for the platform and system and then returns a slice of valid 341 | // instanceConfig. It is possible for the instanceConfig to be empty if no valid 342 | // configurations were specified, however `err` will be set. 343 | func CreateInstanceConfigs(dir string, useFuse bool, instances []string, instancesSrc string, cl *http.Client, skipFailedInstanceConfigs bool) ([]instanceConfig, error) { 344 | if useFuse && !fuse.Supported() { 345 | return nil, errors.New("FUSE not supported on this system") 346 | } 347 | 348 | cfgs, err := parseInstanceConfigs(dir, instances, cl, skipFailedInstanceConfigs) 349 | if err != nil { 350 | return nil, err 351 | } 352 | 353 | if dir == "" { 354 | // Reasons to set '-dir': 355 | // - Using -fuse 356 | // - Using the metadata to get a list of instances 357 | // - Having an instance that uses a 'unix' network 358 | if useFuse { 359 | return nil, errors.New("must set -dir because -fuse was set") 360 | } else if instancesSrc != "" { 361 | return nil, errors.New("must set -dir because -instances_metadata was set") 362 | } else { 363 | for _, v := range cfgs { 364 | if v.Network == "unix" { 365 | return nil, fmt.Errorf("must set -dir: using a unix socket for %v", v.Instance) 366 | } 367 | } 368 | } 369 | // Otherwise it's safe to not set -dir 370 | } 371 | 372 | if useFuse { 373 | if len(instances) != 0 || instancesSrc != "" { 374 | return nil, errors.New("-fuse is not compatible with -projects, -instances, or -instances_metadata") 375 | } 376 | return nil, nil 377 | } 378 | // FUSE disabled. 379 | if len(instances) == 0 && instancesSrc == "" { 380 | // Failure to specifying instance can be caused by following reasons. 381 | // 1. not enough information is provided by flags 382 | // 2. failed to invoke gcloud 383 | var flags string 384 | if fuse.Supported() { 385 | flags = "-projects, -fuse, -instances or -instances_metadata" 386 | } else { 387 | flags = "-projects, -instances or -instances_metadata" 388 | } 389 | 390 | errStr := fmt.Sprintf("no instance selected because none of %s is specified", flags) 391 | return nil, errors.New(errStr) 392 | } 393 | return cfgs, nil 394 | } 395 | --------------------------------------------------------------------------------