How to Handle Multiple NICs in Go: Exclude Link‑Local IPs and Bind Traffic
This article explains why Go programs may pick unusable link‑local addresses on machines with multiple network interfaces, shows how to filter them out, and presents three practical solutions—including per‑NIC binding, Docker container isolation, and DNAT/SNAT rules—to control inbound and outbound traffic.
Multiple NIC Mode Solution for Golang Applications
When registering a gRPC service to etcd, a specific IP address is required, but the correct address is often not obtained.
Local Execution Scenario
In Go, obtaining the local IP address typically uses the following function:
// Get local NIC IP
func getLocalIP() (ipv4 string, err error) {
var (
addrs []net.Addr
addr net.Addr
ipNet *net.IPNet // IP address
isIpNet bool
)
// Get all interfaces
if addrs, err = net.InterfaceAddrs(); err != nil {
return
}
// Take the first non‑loopback IP
for _, addr = range addrs {
if ipNet, isIpNet = addr.(*net.IPNet); isIpNet && !ipNet.IP.IsLoopback() {
// Skip IPv6
if ipNet.IP.To4() != nil {
ipv4 = ipNet.IP.String() // e.g., 192.168.1.1
return
}
}
}
err = ERR_NO_LOCAL_IP_FOUND
return
}On machines with virtual NICs, this code may return an unusable IP address, causing the program to fail at startup.
What is the unusable IP address? It is a link‑local unicast address, which is defined in IPv4 as the 169.254.0.0/16 block and in IPv6 as the fe80::/10 block. Virtual NICs often receive such addresses, so they must be excluded just like loopback addresses.
Modify the filter condition to also exclude link‑local addresses:
if ipNet, isIpNet = addr.(*net.IPNet); isIpNet && !ipNet.IP.IsLoopback() && !ipNet.IP.IsLinkLocalUnicast() {
// process valid IP
}Specifying Inbound/Outbound NIC
If a machine has multiple usable NICs, you may need to bind a specific NIC for traffic. Consider a server with two NICs:
eth0 – 172.31.0.8 (public IP 109.25.48.65) eth1 – 172.31.0.14 (public IP 119.26.38.75)
The following Go example demonstrates how to obtain the listening IP from a flag and display both the local and external IPs:
package main
import (
"flag"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
var addr = flag.String("addr", ":8080", "the http server address")
func init() { flag.Parse() }
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
splits := strings.Split(*addr, ":")
localIP := ""
if len(splits) == 2 { localIP = splits[0] }
res, err := HTTPGet("http://haoip.cn")
if err != nil { panic(err) }
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil { panic(err) }
fmt.Fprintf(w, "NIC IP: %s
", localIP)
fmt.Fprintf(w, "Outbound IP:
%s", body)
})
if err := http.ListenAndServe(*addr, nil); err != nil { panic(err) }
}
func HTTPGet(url string) (*http.Response, error) {
req, _ := http.NewRequest("GET", url, nil)
client := &http.Client{}
req.Header.Set("User-Agent", "curl/7.47.0")
return client.Do(req)
}Solution 1 – Bind Application Traffic to a Specific NIC
Modify the HTTP client to bind the source IP for each request:
func HTTPGet(url, localIP string) (*http.Response, error) {
req, _ := http.NewRequest("GET", url, nil)
client := &http.Client{Transport: &http.Transport{Dial: func(netw, addr string) (net.Conn, error) {
lAddr, err := net.ResolveTCPAddr(netw, localIP+":0")
if err != nil { return nil, err }
rAddr, err := net.ResolveTCPAddr(netw, addr)
if err != nil { return nil, err }
return net.DialTCP(netw, lAddr, rAddr)
}}}
return client.Do(req)
}Run the program with the desired NIC IP:
go run main.go --addr=172.31.0.8:8090
go run main.go --addr=172.31.0.14:8090Solution 2 – Docker Container Isolation
Package the Go app into a Docker image and run each instance on a dedicated bridge network with a fixed container IP:
FROM golang:1.17
WORKDIR /app
COPY . .
RUN go build -o main .
EXPOSE 8080
CMD ["./main"]Create a bridge network:
docker network create --subnet=172.19.0.0/16 --opt "com.docker.network.bridge.name"="eip_bridge" eip_bridgeRun containers bound to specific host NICs:
# Bind to eth0
docker run --network=eip_bridge -p 172.31.0.8:8090:8080 --ip=172.19.0.100 -d eip:latest
# Bind to eth1
docker run --network=eip_bridge -p 172.31.0.14:8090:8080 --ip=172.19.0.101 -d eip:latestBecause all outbound traffic passes through the bridge, use SNAT to rewrite the source address for each container:
sudo iptables -t nat -I POSTROUTING -p all -s 172.19.0.100 -j SNAT --to-source 172.31.0.8
sudo iptables -t nat -I POSTROUTING -p all -s 172.19.0.101 -j SNAT --to-source 172.31.0.14Solution 3 – DNAT/SNAT with Virtual Bridges
Create two virtual bridges (br‑eip1 and br‑eip2) and assign them fixed IPs:
apt-get install bridge-utils
sudo brctl addbr br-eip1
sudo ip link set br-eip1 up
sudo ifconfig br-eip1 172.19.0.100
sudo brctl addbr br-eip2
sudo ip link set br-eip2 up
sudo ifconfig br-eip2 172.19.0.101Redirect inbound traffic to the appropriate bridge using DNAT:
sudo iptables -t nat -I PREROUTING -d 172.31.0.8/32 ! -i br-eip1 -p tcp --dport 8090 -j DNAT --to-destination 172.19.0.100:8090
sudo iptables -t nat -I PREROUTING -d 172.31.0.14/32 ! -i br-eip2 -p tcp --dport 8090 -j DNAT --to-destination 172.19.0.101:8090And rewrite outbound traffic with SNAT:
sudo iptables -t nat -I POSTROUTING -p all -s 172.19.0.100 -j SNAT --to-source 172.31.0.8
sudo iptables -t nat -I POSTROUTING -p all -s 172.19.0.101 -j SNAT --to-source 172.31.0.14These three approaches enable precise control over which NIC a Go application uses for both inbound and outbound traffic, while avoiding problematic link‑local addresses.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
