How to use unix domain socket to make a request when using Gin?

Viewed 185

I am creating a wrapper for firecracker.

To start a VM with firecracker on command line, you have to pass a socket file to the firecracker executable. Something like this:

firecracker --api-sock /path/to/file.socket

Then from another terminal, you can make requests to this server/socket something like this:

curl --unix-socket /tmp/firecracker.socket -XPUT 'http://localhost/actions' -d '{"action_type": "SendCtrlAltDel"}'

I am trying to replicate the same thing from within a Gin server.

I have an endpoint which does the first work, which is to start a server. A minimal code looks like this:

cmd := exec.Command("firecracker", "--api-sock", "/path/to/file.socket")

err := cmd.Start()

This endpoint starts the server and listens for any command. The problem is, I don't know how to use the socket file to make a PUT request to this server. I have found this on the web, but it does not makes much sense to me.

Here is a starter code which does not use any socket file.

func BootSource(c *gin.Context) {
    var body models.BootSource
    c.BindJSON(&body)
    bodyJson, _ := json.Marshal(body)

    // initialize http client
    client := &http.Client{}

    // set the HTTP method, url, and request body
    req, err := http.NewRequest(http.MethodPut, "http://localhost/boot-source", bytes.NewBuffer(bodyJson))
    if err != nil {
        panic(err)
    }

    // set the request header Content-Type for json
    _, err = client.Do(req)
    if err != nil {
        panic(err)
    }
}

How do I make this PUT request use the socket file?

Please also note that I'm using Gin framework.

2 Answers

To do this, you'll need to override the Transport used by your http.Client to configure a function for it to use to create a connection to the socket:

client := http.Client{
  Transport: &http.Transport{
    DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
      return net.DialContext(ctx, "unix", "/path/to/socket") 
    }
  }
}

You can then use that client, and all requests made by it will use that connection. Usually for HTTP services exposed over a socket, the host used in the request is not important, so you can just use any value that makes sense to you e.g

client.Get("http://firecracker/some/api/path")

However, as you are trying to use the Firecracker API, why not just use their SDK: https://github.com/firecracker-microvm/firecracker-go-sdk

This will handle the set up of the connection for you, and prevent you needing to manually craft all of the requests.

Extending to this answer, you can keep the http defaults by cloning default transport

    defaultTransport, ok := http.DefaultTransport.(*http.Transport)
    if !ok {
            panic("http.DefaultTransport is not a *http.Transport")
    }
    unixTransport := defaultTransport.Clone()
    defaultDialContext := unixTransport.DialContext
    unixTransport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
            return defaultDialContext(ctx, "unix", "/path/to/socket")
    }
    client := http.Client{Transport: unixTransport}
    client.Get("http://example.com")
Related