When I began my personal site transition earlier this year, I decided to save some money and take on the challenge of an IPv6-only host. I thought it would be a great way to learn about the current state of IP and pick up new knowledge on IPv6.
I was totally right in my assumption; I’ve learned a lot of new things and gotten frustrated a few times in the process! Most things I’ve ever learned have been centered around IPv4. From university classes to tasks as a software engineer, rarely does an IPv6-only network surface.
One of the issues I encountered is calling an external REST API. Although some APIs are configured with IPv6 in mind, I found that quite a few do not. IPv4 and IPv6 don’t just work together perfectly: they are separate protocols and need some type of translation with a gateway to be understandable to one another. This means an IPv6-only host will need to make some changes: setting up NAT64 with system wide DNS changes in /etc/resolv.conf or using a custom DNS resolver to synthesize IPv6 addresses from the existing IPv4 ones.
Part of my personal site that I enjoy developing is the lightweight API. Currently, this API can be called from the homepage to sign up for email updates from me. To send an email, the handler reaches out to Resend because of its simplicity. Running dig +short aaaa api.resend.com returned no records, so plainly calling the API with their pre-configured client wasn’t working for me.
Custom HTTP client example
Resend offers NewCustomClient, so we can create our own http client that uses a DNS64 resolver to allow us to call api.resend.com.
// return a net http client with a custom dns resolver address
func CustomHttpClient() *http.Client {
// custom resolver with a specific DNS resolver address
resolver := &net.Resolver{
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
dialer := net.Dialer{
Timeout: 3 * time.Second,
DualStack: true,
}
// set dns resolver addr to dns64 resolver such as on trex.fi/2011/dns64.html
// or configure NAT64 and use cloudflare/google
return dialer.DialContext(ctx, "udp", os.Getenv("DNS_RESOLVER_ADDR"))
},
}
// http client using the custom resolver that can work with ipv4
httpClient := &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
ipAddrs, err := resolver.LookupIP(ctx, "ip", host)
if err != nil {
return nil, err
}
return net.Dial(network, net.JoinHostPort(ipAddrs[0].String(), port))
},
},
}
return httpClient
}
With the CustomHttpClient function above, we create a custom HTTP client that uses a custom DNS resolver address. I noted there that a resolver like the ones on trex.fi/2011/dns64.html works good for this use case.
The resolver I noted doesn’t require local NAT64 to be configured, so it’s an easy way to test out connectivity and keep it simple. To use the Cloudflare DNS64 or Google’s public DNS64, NAT64 has to be configured using the well-known prefix. This may involve many more steps to set up, but overall isn’t too difficult.
After deciding to use NAT64 and setting our DNS_RESOLVER_ADDR to a DNS64 resolver or using an integrated resolver, we can create our Resend client and interact with the API.
httpClient := CustomHttpClient()
client := resend.NewCustomClient(httpClient, apiKey)
// ...
sent, err := client.Emails.Send(params)
// ...
Global resolver change
I prefer going the custom client route, but making global nameserver changes is possible too. To do this, you can test by overwriting the resolvers in /etc/resolv.conf. These changes can get overwritten, so permanent changes have to be made in the networking manager being used, such as directly to /etc/network/interfaces or to resolvconf.
Comments
Loading comments...