Golang HTTP客户端通过VPN(openVPN)/设置网络接口

I have to make http requests through VPN. There is php code using cURL making what I need, nothing additional:

curl_setopt($cu, CURLOPT_INTERFACE, "tun0");

This interface from ifconfig:

 tun0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST>  mtu 1500
        inet 10.13.13.2  netmask 255.255.255.255  destination 10.13.13.1
        inet6 fe80::80ee:132:d102:a52d  prefixlen 64  scopeid 0x20<link>
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  txqueuelen 100  (UNSPEC)            

My go code (as I understand, I should bind to ip of tun0):

package main

import (
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "time"
)

func main() {
    httpTransport := &http.Transport{}
    ief, err := net.InterfaceByName("tun0")
    if err != nil {
        log.Fatal(err)
    }
    addrs, err := ief.Addrs()
    if err != nil {
        log.Fatal(err)
    }
    tcpAddr := &net.TCPAddr{IP: addrs[0].(*net.IPNet).IP}
    println(tcpAddr.String())
    httpTransport.Dial = (&net.Dialer{
        Timeout:   20 * time.Second,
        LocalAddr: tcpAddr,
    }).Dial

    c := &http.Client{
        Transport: httpTransport,
        Timeout:   30 * time.Second,
    }

    req, err := http.NewRequest(
        http.MethodGet,
        "http://example.com/",
        nil,
    )
    if err != nil {
        log.Fatal(err)
    }

    resp, err := c.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }
    println(string(bytes))
}

So what I get :

10.13.13.2:0
2019/03/21 15:29:42 Get http://example.com/: dial tcp 10.13.13.2:0->93.184.216.34:80: i/o timeout
exit status 1

Php code with cUrl gets this page fast. I tried many times with Go code, so timeouts can do nothing. Any ideas how to replace that one PHP string in Go?

UPDATE

PHP code getting same page via tun0 and it's output:

<?php
$cu = curl_init();
curl_setopt($cu, CURLOPT_URL, "http://example.com");
curl_setopt($cu, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($cu, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($cu, CURLOPT_TIMEOUT, 30);
curl_setopt($cu, CURLOPT_INTERFACE, "tun0");
$result = curl_exec($cu);;
curl_close($cu);
echo $result . PHP_EOL;

Output:

<!doctype html>
<html>
<head>
    <title>Example Domain</title>

    <meta charset="utf-8" />
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <style type="text/css">
    body {
        background-color: #f0f0f2;
        margin: 0;
        padding: 0;
        font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;

    }
    div {
        width: 600px;
        margin: 5em auto;
        padding: 50px;
        background-color: #fff;
        border-radius: 1em;
    }
    a:link, a:visited {
        color: #38488f;
        text-decoration: none;
    }
    @media (max-width: 700px) {
        body {
            background-color: #fff;
        }
        div {
            width: auto;
            margin: 0 auto;
            border-radius: 0;
            padding: 1em;
        }
    }
    </style>    
</head>

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is established to be used for illustrative examples in documents. You may use this
    domain in examples without prior coordination or asking for permission.</p>
    <p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>

I have a working example:

package main

import (
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
)

func main() {
    ifname := "tun0"
    iface, err := net.InterfaceByName(ifname)
    if err != nil {
        panic(fmt.Sprintf("could not get interface: %s", err))
    }

    addrs, err := iface.Addrs()
    if err != nil {
        panic(fmt.Sprintf("could not get addresses from interface: %s", err))
    }

    fmt.Printf("%+v
", addrs)

    c := &http.Client{
        Transport: &http.Transport{
            Dial: (&net.Dialer{
                LocalAddr: &net.TCPAddr{
                    IP: addrs[0].(*net.IPNet).IP,
                    //IP: net.ParseIP("192.168.0.2"),
                    //IP: net.ParseIP("127.0.0.1"),
                },
            }).Dial,
        },
    }

    r, err := c.Get("http://localhost/ip")
    if err != nil {
        panic(fmt.Sprintf("couldn't get http:%s", err))
    }
    b, err := ioutil.ReadAll(r.Body)
    if err != nil {
        panic(fmt.Sprintf("could not read body:%s", err))
    }
    fmt.Printf("%s", b)
}

You can see in the LocalAddrcommented different addresses for my tests in my environment.

I used a local running httpbin and I can see this:

$ go run main.go
[myipv6 myipv4]
{"origin":"192.168.0.2"}

Here, the 2nd line of output is the response from the server. If I change a bit the code to force 127.0.0.1 (as commented), I get this:

$ go run main.go
[myipv6 myipv4]
{"origin":"127.0.0.1"}

Which means, the server does indeed see a different source address.

I think, your code works but as you see in the output you have an I/O timeout. The cause of this might be because your VPN connection is not allowed to connect to your destination. The program seems to do the right connection you asked it, the message dial tcp 10.13.13.2:0->93.184.216.34:80 which is correct according you your ifconfig output.

Also, if I especially connect to a closed port I get this:

panic: couldn't get http:Get http://localhost:81/ip: dial tcp 192.168.0.2:0->127.0.0.1:81: connect: connection refused

Which shows that your addresses are correct.