2

I am using the go-rod library to do some web automation, this service that I am making will live inside a container, and for local debugging I want to be able to connect to the browser I have running locally. This issue is that --remote-debugging-address flag only works with --headless flag. This is a big issue for me as I need to inspect and look at the browser while developing. I've read that SSH tunneling can be done but I am unable to get it working. I tried all combinations of flags, ports and hosts and all result in some kind of error.

Current setup

  • Running the chromium instance on my host chromium --remote-debugging-port=9222. Which gets me an address like so DevTools listening on ws://0.0.0.0:9222/devtools/browser/f66524d5-eecb-44c2-a48c-5b14d8e6d998

  • Running my app via this script

#!/bin/bash
docker build -t rod-test .
docker run --add-host=host.docker.internal:host-gateway --rm rod-test

The docker file

FROM golang:1.16-alpine

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

RUN go build -o /rod

CMD [ "/rod" ]

The main.go

package main

import (
    "fmt"

    "github.com/go-rod/rod"
)

func main() {
    browser := rod.New().ControlURL("ws://host.docker.internal:9222/devtools/browser/f66524d5-eecb-44c2-a48c-5b14d8e6d998")
    if err := browser.Connect(); err != nil {
        fmt.Printf("err while connecting: %v", err)
        return
    }

    fmt.Println(
        browser.MustPage("https://mdn.dev/").MustEval("() => document.title"),
    )
}

If I use --headless --remote-debugging-address=0.0.0.0 it works, but if I remove the headless part it refuses the connection. The only solution seems to be to use SSH tunneling like it is mentioned here. But these keep erroring out for me as all of the answers are very vague as to what is what and what IP should go where

$ ssh -L 172.17.0.1:9222:localhost:9222 -N localhost
ssh: connect to host localhost port 22: Connection refused

OR

$ ssh -L 172.17.0.1:9222:localhost:9222                            
usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface]
           [-b bind_address] [-c cipher_spec] [-D [bind_address:]port]
           [-E log_file] [-e escape_char] [-F configfile] [-I pkcs11]
           [-i identity_file] [-J [user@]host[:port]] [-L address]
           [-l login_name] [-m mac_spec] [-O ctl_cmd] [-o option] [-p port]
           [-Q query_option] [-R address] [-S ctl_path] [-W host:port]
           [-w local_tun[:remote_tun]] destination [command]

What I want to happen is to be able to connect from the container to the debugger running on my host machine. Some caveats that I would like to cover are

  • It works on other platforms not just linux
  • It doesn't require complex setup from the user

This will be used by other teammates and it would be nice to have an approachable setup

Nikola-Milovic
  • 1,393
  • 1
  • 12
  • 35

1 Answers1

0

So anyone that might have a similar usecase I'll save you a couple of days of debugging and trial and error. The solution I came upon is the following

Using this docker image with chrome debugger pointing at 9222 port. You can run this as is or put it inside docker compose as I did. Now if you navigate to this url chrome://inspect/#devices while the above mentioned image is running, you'll be able to access the and view the browser inside the container. (If its running on 9222 then you can even do localhost:9222 and it will show you the available pages)

Now the code to connect to it using go-rod

        ips, err := net.LookupIP("browser-devtools")
        if err != nil {
            return nil, fmt.Errorf("failed to get browser service ip: %w", err)
        }
        if len(ips) == 0 {
            return nil, errors.New("ip list empty")
        }

        ip := ips[0].String()
        fmt.Printf("IP is %q\n", ip)

        resp, err := http.Get(fmt.Sprintf("http://%s:9222/json/version/", ip))
        if err != nil {
            return nil, fmt.Errorf("failed to get devtools info: %w", err)
        }

        defer resp.Body.Close()
        body, err := io.ReadAll(resp.Body)
        if err != nil {
            return nil, fmt.Errorf("failed read response body: %w", err)
        }

        println(string(body))

        responseMapped := make(map[string]interface{})
        if err = json.Unmarshal(body, &responseMapped); err != nil {
            return nil, fmt.Errorf("failed to unmarshal response: %w", err)
        }

        debuggerUrl, ok := responseMapped["webSocketDebuggerUrl"].(string)
        if !ok {
            return nil, errors.New("no 'webSocketDebuggerUrl' entry in response map")
        }

        browser := rod.New().ControlURL(debuggerUrl)
        if err := browser.Connect(); err != nil {
            return nil, fmt.Errorf("failed connect to browser: %w", err)
        }

        return browser, nil

Explanation:

ips, err := net.LookupIP("browser-devtools")

There is a slight issue that only localhost host can access the debugger or any (allowed) numerical IP, so basically you cannot simply use the service name in docker compose to access the browser as it will be denied. Here we are resolving the actual IP of the service with a lookup

ip := ips[0].String()

resp, err := http.Get(fmt.Sprintf("http://%s:9222/json/version/", ip))

Here we get the string representation of the IP (something like 127.1.0.0) and pass it into the request. The request in itself is important as we don't have the actual browser debugger url that go-rod expects so this get request will return

{
    "Browser": "Chrome/72.0.3601.0",
    "Protocol-Version": "1.3",
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3601.0 Safari/537.36",
    "V8-Version": "7.2.233",
    "WebKit-Version": "537.36 (@cfede9db1d154de0468cb0538479f34c0755a0f4)",
    "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/b0b8a4fb-bb17-4359-9533-a8d9f3908bd8"
}

Something along these lines. And the webSocketDebuggerUrl is the actual url we're connecting to

Nikola-Milovic
  • 1,393
  • 1
  • 12
  • 35