Golang 中网络请求使用指定网卡
By admin
- 4 minutes read - 844 words当用户发起一个网络请求时,流量会通过默认的网卡接口流出与流入,但有时需要将流量通过指定的网卡进行流出流入,这时我们可能需要进行一些额外的开发工作,对其实现主要用到了 Dialer.Control
配置项。
type Dialer struct {
// If Control is not nil, it is called after creating the network
// connection but before actually dialing.
//
// Network and address parameters passed to Control method are not
// necessarily the ones passed to Dial. For example, passing "tcp" to Dial
// will cause the Control function to be called with "tcp4" or "tcp6".
Control func(network, address string, c syscall.RawConn) error
}
可以看到这是一个函数类型的参数。
环境
当前系统一共两个网卡 ens33
和 ens160
,ip地址分别为 192.168.3.80
和 192.168.3.48
➜ ~ ifconfig
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.3.80 netmask 255.255.255.0 broadcast 192.168.3.255
inet6 fe80::8091:2406:c51e:ecb9 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:4f:05:90 txqueuelen 1000 (Ethernet)
RX packets 4805008 bytes 826619853 (826.6 MB)
RX errors 0 dropped 104152 overruns 0 frame 0
TX packets 732513 bytes 284605386 (284.6 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
ens160: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.3.48 netmask 255.255.255.0 broadcast 192.168.3.255
inet6 fe80::259a:d8d4:80a9:7fa4 prefixlen 64 scopeid 0x20<link>
ether 00:0c:29:4f:05:9a txqueuelen 1000 (Ethernet)
RX packets 4158530 bytes 746167179 (746.1 MB)
RX errors 1 dropped 106875 overruns 0 frame 0
TX packets 351616 bytes 149235606 (149.2 MB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
路由表记录
➜ ~ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.3.1 0.0.0.0 UG 100 0 0 ens33
0.0.0.0 192.168.3.1 0.0.0.0 UG 101 0 0 ens160
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 ens33
192.168.3.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33
192.168.3.0 0.0.0.0 255.255.255.0 U 101 0 0 ens160
从最后两条路由记录可以看到对于 192.168.3.0/24
这个段的流量会匹配的两个物理网卡,但由于 第一条跌幅配置的 Metric
的优先级比较的高,因此最终流量只会走网卡 ens33
。
我们可以在另一台机器 192.168.3.58
用 python3
搭建一个HTTPServer
,用命令 python3 -m http.server 8080
即可快速启用一个webserver,这时在这台机器用 curl
发送一个请求。
curl -v http://web.test.com:8080/test/index.html
然后看下python3 的日志
sxf@sxf-virtual-machine:/data/8080$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
192.168.3.80 - - [16/Jan/2023 15:08:18] "GET /test/index.html HTTP/1.1" 200 -
可以看到客户端的IP 192.168.3.80
正是 ens33
网卡的IP,即当前流量是经过默认网卡流出。
下面我们通过程序来实现让其流量走 ens160
网卡。
实现
下面我们再看一下如何用 Golang 来指定网卡,为了方便这里直接贴出来完整的代码
package main
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"net/url"
"syscall"
"golang.org/x/sys/unix"
"net"
)
var interfaceName string = "ens160" // ens33、ens160
const (
network string = "tcp"
address string = "192.168.3.58:8080"
)
func main() {
// 重要核心代码
d := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
return setSocketOptions(network, address, c, interfaceName)
},
}
ctx := context.Background()
// 拨号获取一个连接
conn, err := d.DialContext(ctx, network, address)
if err != nil {
panic(err)
}
// 1. create new request
reqURL, _ := url.Parse("http://web.test.com/test/index.html")
hdr := http.Header{}
req := &http.Request{
Method: "GET",
URL: reqURL,
Header: hdr,
}
err = req.Write(conn)
if err != nil {
panic(err)
}
// 2. Get a response
r := bufio.NewReader(conn)
resp, err := http.ReadResponse(r, req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
status := resp.StatusCode
body, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(conn.LocalAddr())
for name, vv := range resp.Header {
fmt.Print(name, ":")
for _, v := range vv {
fmt.Print(v)
}
fmt.Println()
}
fmt.Println()
fmt.Println(status)
fmt.Println(string(body))
}
func isTCPSocket(network string) bool {
switch network {
case "tcp", "tcp4", "tcp6":
return true
default:
return false
}
}
func isUDPSocket(network string) bool {
switch network {
case "udp", "udp4", "udp6":
return true
default:
return false
}
}
func setSocketOptions(network, address string, c syscall.RawConn, interfaceName string) (err error) {
if interfaceName == "" || !isTCPSocket(network) && !isUDPSocket(network) {
return
}
var innerErr error
err = c.Control(func(fd uintptr) {
host, _, _ := net.SplitHostPort(address)
if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
return
}
if interfaceName != "" {
if innerErr = unix.BindToDevice(int(fd), interfaceName); innerErr != nil {
return
}
}
})
if innerErr != nil {
err = innerErr
}
return
}
我们首先自定义了 Dialer.Control
的实现
d := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
return setSocketOptions(network, address, c, interfaceName)
},
}
这里指定了自定义实现函数setSocketOptions
, 下面是实现
func setSocketOptions(network, address string, c syscall.RawConn, interfaceName string) (err error) {
if interfaceName == "" || !isTCPSocket(network) && !isUDPSocket(network) {
return
}
var innerErr error
err = c.Control(func(fd uintptr) {
host, _, _ := net.SplitHostPort(address)
if ip := net.ParseIP(host); ip != nil && !ip.IsGlobalUnicast() {
return
}
// 核心代理
if interfaceName != "" {
if innerErr = unix.BindToDevice(int(fd), interfaceName); innerErr != nil {
return
}
}
})
if innerErr != nil {
err = innerErr
}
return
}
这里重点是使用了 unix.BindToDevice()
函数来指定了要使用的物理网卡,其参数类型是 int
整型,其实现为官方库
// BindToDevice binds the socket associated with fd to device.
func BindToDevice(fd int, device string) (err error) {
return SetsockoptString(fd, SOL_SOCKET, SO_BINDTODEVICE, device)
}
func SetsockoptString(fd, level, opt int, s string) (err error) {
var p unsafe.Pointer
if len(s) > 0 {
p = unsafe.Pointer(&[]byte(s)[0])
}
return setsockopt(fd, level, opt, p, uintptr(len(s)))
}
func setsockopt(s int, level int, name int, val unsafe.Pointer, vallen uintptr) (err error) {
_, _, e1 := Syscall6(SYS_SETSOCKOPT, uintptr(s), uintptr(level), uintptr(name), uintptr(val), uintptr(vallen), 0)
if e1 != 0 {
err = errnoErr(e1)
}
return
}
通过函数一级级的调用,最终调用了 Syscall6
函数,到这里基本可以不用再往下跟踪了,个人觉得只要跟踪到 setsocketopt
函数就可以了,对于写 c 的同学来说,对于这个函数一点也不陌生。
此时运行上面的程序,在 webserver 的日志里可以看到客户端的ip就是上面程序指定网卡 ens160
的ip地址。
另外,上面发送的 http 请求的方法并没有使用大家经常使用的 client.Do()
,而是使用了,先建立一个 net.Conn
链接,然后将一个 http.Request
请求写入这个conn ,以此来表示请求。同样对于响应是通过 http.ReadResponse()
来实现,这两种方式都可以使用的。
总结
在一些网络底层基本都会用到 Dialer.Control
这个字段配置,在C中这么多通过 setsocketopt
实现的方法都是在这里实现的,这里推荐大家参考一下耗子叔以前写的一篇博客 从一次经历谈 TIME_WAIT 的那些事 ,采用的也是类似的方法。