Repository: hetznercloud/hcloud-go
Pull request #183 (2021-08-20 06:45:36)

Rdns client


Patch:
diff --git a/hcloud/client.go b/hcloud/client.go
index a4bb337..8f5bd4e 100644
--- a/hcloud/client.go
+++ b/hcloud/client.go
@@ -75,6 +75,7 @@ type Client struct {
 	SSHKey           SSHKeyClient
 	Volume           VolumeClient
 	PlacementGroup   PlacementGroupClient
+	RDNS             RDNSClient
 }
 
 // A ClientOption is used to configure a Client.
@@ -166,6 +167,7 @@ func NewClient(options ...ClientOption) *Client {
 	client.Certificate = CertificateClient{client: client}
 	client.Firewall = FirewallClient{client: client}
 	client.PlacementGroup = PlacementGroupClient{client: client}
+	client.RDNS = RDNSClient{client: client}
 
 	return client
 }
diff --git a/hcloud/error.go b/hcloud/error.go
index d54de70..d7cb750 100644
--- a/hcloud/error.go
+++ b/hcloud/error.go
@@ -1,6 +1,9 @@
 package hcloud
 
-import "fmt"
+import (
+	"fmt"
+	"net"
+)
 
 // ErrorCode represents an error code returned from the API.
 type ErrorCode string
@@ -103,3 +106,19 @@ func IsError(err error, code ErrorCode) bool {
 	apiErr, ok := err.(Error)
 	return ok && apiErr.Code == code
 }
+
+type InvalidIPError struct {
+	IP string
+}
+
+func (e InvalidIPError) Error() string {
+	return fmt.Sprintf("could not parse ip address %s", e.IP)
+}
+
+type DNSNotFoundError struct {
+	IP net.IP
+}
+
+func (e DNSNotFoundError) Error() string {
+	return fmt.Sprintf("dns for ip %s not found", e.IP.String())
+}
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index d8a6d0f..c6bced5 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -32,6 +32,7 @@ type FloatingIP struct {
 }
 
 // DNSPtrForIP returns the reverse DNS pointer of the IP address.
+// Deprecated: Use GetDNSPtrForIP instead
 func (f *FloatingIP) DNSPtrForIP(ip net.IP) string {
 	return f.DNSPtr[ip.String()]
 }
@@ -50,6 +51,43 @@ const (
 	FloatingIPTypeIPv6 FloatingIPType = "ipv6"
 )
 
+// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
+// Pass a nil ptr to reset the reverse DNS pointer to its default value.
+func (f *FloatingIP) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) {
+	reqBody := schema.FloatingIPActionChangeDNSPtrRequest{
+		IP:     ip.String(),
+		DNSPtr: ptr,
+	}
+	reqBodyData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", f.ID)
+	req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	respBody := schema.FloatingIPActionChangeDNSPtrResponse{}
+	resp, err := client.Do(req, &respBody)
+	if err != nil {
+		return nil, resp, err
+	}
+	return ActionFromSchema(respBody.Action), resp, nil
+}
+
+// GetDNSPtrForIP searches for the dns assigned to the given IP address.
+// It returns an error if there is no dns set for the given IP address.
+func (f *FloatingIP) GetDNSPtrForIP(ip net.IP) (string, error) {
+	dns, ok := f.DNSPtr[ip.String()]
+	if !ok {
+		return "", DNSNotFoundError{ip}
+	}
+
+	return dns, nil
+}
+
 // FloatingIPClient is a client for the Floating IP API.
 type FloatingIPClient struct {
 	client *Client
@@ -325,27 +363,11 @@ func (c *FloatingIPClient) Unassign(ctx context.Context, floatingIP *FloatingIP)
 // ChangeDNSPtr changes or resets the reverse DNS pointer for a Floating IP address.
 // Pass a nil ptr to reset the reverse DNS pointer to its default value.
 func (c *FloatingIPClient) ChangeDNSPtr(ctx context.Context, floatingIP *FloatingIP, ip string, ptr *string) (*Action, *Response, error) {
-	reqBody := schema.FloatingIPActionChangeDNSPtrRequest{
-		IP:     ip,
-		DNSPtr: ptr,
+	netIP := net.ParseIP(ip)
+	if netIP == nil {
+		return nil, nil, InvalidIPError{ip}
 	}
-	reqBodyData, err := json.Marshal(reqBody)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	path := fmt.Sprintf("/floating_ips/%d/actions/change_dns_ptr", floatingIP.ID)
-	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
-	if err != nil {
-		return nil, nil, err
-	}
-
-	respBody := schema.FloatingIPActionChangeDNSPtrResponse{}
-	resp, err := c.client.Do(req, &respBody)
-	if err != nil {
-		return nil, resp, err
-	}
-	return ActionFromSchema(respBody.Action), resp, nil
+	return floatingIP.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr)
 }
 
 // FloatingIPChangeProtectionOpts specifies options for changing the resource protection level of a Floating IP.
diff --git a/hcloud/hcloud.go b/hcloud/hcloud.go
index 78e2594..5833212 100644
--- a/hcloud/hcloud.go
+++ b/hcloud/hcloud.go
@@ -2,4 +2,4 @@
 package hcloud
 
 // Version is the library's version following Semantic Versioning.
-const Version = "1.31.0"
+const Version = "1.31.1"
diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go
index 7e76d36..2858c81 100644
--- a/hcloud/load_balancer.go
+++ b/hcloud/load_balancer.go
@@ -197,6 +197,44 @@ type LoadBalancerProtection struct {
 	Delete bool
 }
 
+// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
+// Pass a nil ptr to reset the reverse DNS pointer to its default value.
+func (lb *LoadBalancer) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) {
+	reqBody := schema.LoadBalancerActionChangeDNSPtrRequest{
+		IP:     ip.String(),
+		DNSPtr: ptr,
+	}
+	reqBodyData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	path := fmt.Sprintf("/load_balancers/%d/actions/change_dns_ptr", lb.ID)
+	req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	respBody := schema.LoadBalancerActionChangeDNSPtrResponse{}
+	resp, err := client.Do(req, &respBody)
+	if err != nil {
+		return nil, resp, err
+	}
+	return ActionFromSchema(respBody.Action), resp, nil
+}
+
+// GetDNSPtrForIP searches for the dns assigned to the given IP address.
+// It returns an error if there is no dns set for the given IP address.
+func (lb *LoadBalancer) GetDNSPtrForIP(ip net.IP) (string, error) {
+	if net.IP.Equal(lb.PublicNet.IPv4.IP, ip) {
+		return lb.PublicNet.IPv4.DNSPtr, nil
+	} else if net.IP.Equal(lb.PublicNet.IPv6.IP, ip) {
+		return lb.PublicNet.IPv6.DNSPtr, nil
+	}
+
+	return "", DNSNotFoundError{ip}
+}
+
 // LoadBalancerClient is a client for the Load Balancers API.
 type LoadBalancerClient struct {
 	client *Client
@@ -1030,25 +1068,9 @@ func (c *LoadBalancerClient) GetMetrics(
 // ChangeDNSPtr changes or resets the reverse DNS pointer for a Load Balancer.
 // Pass a nil ptr to reset the reverse DNS pointer to its default value.
 func (c *LoadBalancerClient) ChangeDNSPtr(ctx context.Context, lb *LoadBalancer, ip string, ptr *string) (*Action, *Response, error) {
-	reqBody := schema.LoadBalancerActionChangeDNSPtrRequest{
-		IP:     ip,
-		DNSPtr: ptr,
-	}
-	reqBodyData, err := json.Marshal(reqBody)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	path := fmt.Sprintf("/load_balancers/%d/actions/change_dns_ptr", lb.ID)
-	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
-	if err != nil {
-		return nil, nil, err
+	netIP := net.ParseIP(ip)
+	if netIP == nil {
+		return nil, nil, InvalidIPError{ip}
 	}
-
-	respBody := schema.LoadBalancerActionChangeDNSPtrResponse{}
-	resp, err := c.client.Do(req, &respBody)
-	if err != nil {
-		return nil, resp, err
-	}
-	return ActionFromSchema(respBody.Action), resp, nil
+	return lb.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr)
 }
diff --git a/hcloud/rdns.go b/hcloud/rdns.go
new file mode 100644
index 0000000..70d31f9
--- /dev/null
+++ b/hcloud/rdns.go
@@ -0,0 +1,46 @@
+package hcloud
+
+import (
+	"context"
+	"fmt"
+	"net"
+)
+
+// RDNSSupporter defines functions to change and lookup reverse dns entries.
+// currently implemented by Server, FloatingIP and LoadBalancer
+type RDNSSupporter interface {
+	// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
+	// Pass a nil ptr to reset the reverse DNS pointer to its default value.
+	changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error)
+	// GetDNSPtrForIP searches for the dns assigned to the given IP address.
+	// It returns an error if there is no dns set for the given IP address.
+	GetDNSPtrForIP(ip net.IP) (string, error)
+}
+
+// RDNSClient simplifys the handling objects which support reverse dns entries.
+type RDNSClient struct {
+	client *Client
+}
+
+// ChangeDNSPtr changes or resets the reverse DNS pointer for a IP address.
+// Pass a nil ptr to reset the reverse DNS pointer to its default value.
+func (c *RDNSClient) ChangeDNSPtr(ctx context.Context, rdns RDNSSupporter, ip net.IP, ptr *string) (*Action, *Response, error) {
+	return rdns.changeDNSPtr(ctx, c.client, ip, ptr)
+}
+
+// SupportsRDNS checks if the object supports reverse dns functions.
+func SupportsRDNS(i interface{}) bool {
+	_, ok := i.(RDNSSupporter)
+	return ok
+}
+
+// RDNSLookup searches for the dns assigned to the given IP address.
+// It returns an error if the object does not support reverse dns or if there is no dns set for the given IP address.
+func RDNSLookup(i interface{}, ip net.IP) (string, error) {
+	rdns, ok := i.(RDNSSupporter)
+	if !ok {
+		return "", fmt.Errorf("%+v does not support RDNS", i)
+	}
+
+	return rdns.GetDNSPtrForIP(ip)
+}
diff --git a/hcloud/server.go b/hcloud/server.go
index c09cecb..4a0f2b1 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -141,6 +141,44 @@ const (
 	ServerRescueTypeFreeBSD64 ServerRescueType = "freebsd64"
 )
 
+// changeDNSPtr changes or resets the reverse DNS pointer for a IP address.
+// Pass a nil ptr to reset the reverse DNS pointer to its default value.
+func (s *Server) changeDNSPtr(ctx context.Context, client *Client, ip net.IP, ptr *string) (*Action, *Response, error) {
+	reqBody := schema.ServerActionChangeDNSPtrRequest{
+		IP:     ip.String(),
+		DNSPtr: ptr,
+	}
+	reqBodyData, err := json.Marshal(reqBody)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", s.ID)
+	req, err := client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
+	if err != nil {
+		return nil, nil, err
+	}
+
+	respBody := schema.ServerActionChangeDNSPtrResponse{}
+	resp, err := client.Do(req, &respBody)
+	if err != nil {
+		return nil, resp, err
+	}
+	return ActionFromSchema(respBody.Action), resp, nil
+}
+
+// GetDNSPtrForIP searches for the dns assigned to the given IP address.
+// It returns an error if there is no dns set for the given IP address.
+func (s *Server) GetDNSPtrForIP(ip net.IP) (string, error) {
+	if net.IP.Equal(s.PublicNet.IPv4.IP, ip) {
+		return s.PublicNet.IPv4.DNSPtr, nil
+	} else if dns, ok := s.PublicNet.IPv6.DNSPtr[ip.String()]; ok {
+		return dns, nil
+	}
+
+	return "", DNSNotFoundError{ip}
+}
+
 // ServerClient is a client for the servers API.
 type ServerClient struct {
 	client *Client
@@ -808,27 +846,11 @@ func (c *ServerClient) ChangeType(ctx context.Context, server *Server, opts Serv
 // ChangeDNSPtr changes or resets the reverse DNS pointer for a server IP address.
 // Pass a nil ptr to reset the reverse DNS pointer to its default value.
 func (c *ServerClient) ChangeDNSPtr(ctx context.Context, server *Server, ip string, ptr *string) (*Action, *Response, error) {
-	reqBody := schema.ServerActionChangeDNSPtrRequest{
-		IP:     ip,
-		DNSPtr: ptr,
+	netIP := net.ParseIP(ip)
+	if netIP == nil {
+		return nil, nil, InvalidIPError{ip}
 	}
-	reqBodyData, err := json.Marshal(reqBody)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	path := fmt.Sprintf("/servers/%d/actions/change_dns_ptr", server.ID)
-	req, err := c.client.NewRequest(ctx, "POST", path, bytes.NewReader(reqBodyData))
-	if err != nil {
-		return nil, nil, err
-	}
-
-	respBody := schema.ServerActionChangeDNSPtrResponse{}
-	resp, err := c.client.Do(req, &respBody)
-	if err != nil {
-		return nil, resp, err
-	}
-	return ActionFromSchema(respBody.Action), resp, nil
+	return server.changeDNSPtr(ctx, c.client, net.ParseIP(ip), ptr)
 }
 
 // ServerChangeProtectionOpts specifies options for changing the resource protection level of a server.


Test patch:
diff --git a/hcloud/floating_ip_test.go b/hcloud/floating_ip_test.go
index eafcc85..eb1709e 100644
--- a/hcloud/floating_ip_test.go
+++ b/hcloud/floating_ip_test.go
@@ -535,75 +535,6 @@ func TestFloatingIPClientUnassign(t *testing.T) {
 	}
 }
 
-func TestFloatingIPClientChangeDNSPtr(t *testing.T) {
-	var (
-		ctx        = context.Background()
-		floatingIP = &FloatingIP{ID: 1}
-	)
-
-	t.Run("set", func(t *testing.T) {
-		env := newTestEnv()
-		defer env.Teardown()
-
-		env.Mux.HandleFunc("/floating_ips/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) {
-			var reqBody schema.FloatingIPActionChangeDNSPtrRequest
-			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
-				t.Fatal(err)
-			}
-			if reqBody.IP != "127.0.0.1" {
-				t.Errorf("unexpected IP: %v", reqBody.IP)
-			}
-			if reqBody.DNSPtr == nil || *reqBody.DNSPtr != "example.com" {
-				t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
-			}
-			json.NewEncoder(w).Encode(schema.FloatingIPActionChangeDNSPtrResponse{
-				Action: schema.Action{
-					ID: 1,
-				},
-			})
-		})
-
-		action, _, err := env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, "127.0.0.1", String("example.com"))
-		if err != nil {
-			t.Fatal(err)
-		}
-		if action.ID != 1 {
-			t.Errorf("unexpected action ID: %d", action.ID)
-		}
-	})
-
-	t.Run("reset", func(t *testing.T) {
-		env := newTestEnv()
-		defer env.Teardown()
-
-		env.Mux.HandleFunc("/floating_ips/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) {
-			var reqBody schema.FloatingIPActionChangeDNSPtrRequest
-			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
-				t.Fatal(err)
-			}
-			if reqBody.IP != "127.0.0.1" {
-				t.Errorf("unexpected IP: %v", reqBody.IP)
-			}
-			if reqBody.DNSPtr != nil {
-				t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
-			}
-			json.NewEncoder(w).Encode(schema.FloatingIPActionChangeDNSPtrResponse{
-				Action: schema.Action{
-					ID: 1,
-				},
-			})
-		})
-
-		action, _, err := env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, "127.0.0.1", nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if action.ID != 1 {
-			t.Errorf("unexpected action ID: %d", action.ID)
-		}
-	})
-}
-
 func TestFloatingIPClientChangeProtection(t *testing.T) {
 	var (
 		ctx        = context.Background()
diff --git a/hcloud/load_balancer_test.go b/hcloud/load_balancer_test.go
index 587b778..e414831 100644
--- a/hcloud/load_balancer_test.go
+++ b/hcloud/load_balancer_test.go
@@ -1249,72 +1249,3 @@ func loadBalancerMetricsOptsFromURL(t *testing.T, u *url.URL) LoadBalancerGetMet
 
 	return opts
 }
-
-func TestLoadBalancerClientChangeDNSPtr(t *testing.T) {
-	var (
-		ctx          = context.Background()
-		loadBalancer = &LoadBalancer{ID: 1}
-	)
-
-	t.Run("set", func(t *testing.T) {
-		env := newTestEnv()
-		defer env.Teardown()
-
-		env.Mux.HandleFunc("/load_balancers/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) {
-			var reqBody schema.LoadBalancerActionChangeDNSPtrRequest
-			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
-				t.Fatal(err)
-			}
-			if reqBody.IP != "127.0.0.1" {
-				t.Errorf("unexpected IP: %v", reqBody.IP)
-			}
-			if reqBody.DNSPtr == nil || *reqBody.DNSPtr != "example.com" {
-				t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
-			}
-			json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeDNSPtrResponse{
-				Action: schema.Action{
-					ID: 1,
-				},
-			})
-		})
-
-		action, _, err := env.Client.LoadBalancer.ChangeDNSPtr(ctx, loadBalancer, "127.0.0.1", String("example.com"))
-		if err != nil {
-			t.Fatal(err)
-		}
-		if action.ID != 1 {
-			t.Errorf("unexpected action ID: %d", action.ID)
-		}
-	})
-
-	t.Run("reset", func(t *testing.T) {
-		env := newTestEnv()
-		defer env.Teardown()
-
-		env.Mux.HandleFunc("/load_balancers/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) {
-			var reqBody schema.LoadBalancerActionChangeDNSPtrRequest
-			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
-				t.Fatal(err)
-			}
-			if reqBody.IP != "127.0.0.1" {
-				t.Errorf("unexpected IP: %v", reqBody.IP)
-			}
-			if reqBody.DNSPtr != nil {
-				t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
-			}
-			json.NewEncoder(w).Encode(schema.LoadBalancerActionChangeDNSPtrResponse{
-				Action: schema.Action{
-					ID: 1,
-				},
-			})
-		})
-
-		action, _, err := env.Client.LoadBalancer.ChangeDNSPtr(ctx, loadBalancer, "127.0.0.1", nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if action.ID != 1 {
-			t.Errorf("unexpected action ID: %d", action.ID)
-		}
-	})
-}
diff --git a/hcloud/rdns_test.go b/hcloud/rdns_test.go
new file mode 100644
index 0000000..dc15b9e
--- /dev/null
+++ b/hcloud/rdns_test.go
@@ -0,0 +1,274 @@
+package hcloud
+
+import (
+	"context"
+	"encoding/json"
+	"net"
+	"net/http"
+	"testing"
+
+	"github.com/hetznercloud/hcloud-go/hcloud/schema"
+)
+
+func TestChangeDNSPtr(t *testing.T) {
+	var (
+		ctx          = context.Background()
+		server       = &Server{ID: 1}
+		loadBalancer = &LoadBalancer{ID: 1}
+		floatingIP   = &FloatingIP{ID: 1}
+		dns          = "example.com"
+	)
+
+	tests := []struct {
+		name       string
+		apiURL     string
+		IP         net.IP
+		DNS        *string
+		changeFunc func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error)
+	}{
+		{
+			name:   "set via server client",
+			apiURL: "/servers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    &dns,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.Server.ChangeDNSPtr(ctx, server, ip.String(), dns)
+			},
+		},
+		{
+			name:   "reset via server client",
+			apiURL: "/servers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    nil,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.Server.ChangeDNSPtr(ctx, server, ip.String(), dns)
+			},
+		},
+		{
+			name:   "set server via rdns client",
+			apiURL: "/servers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    &dns,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.RDNS.ChangeDNSPtr(ctx, server, ip, dns)
+			},
+		},
+		{
+			name:   "reset server via rdns client",
+			apiURL: "/servers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    nil,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.RDNS.ChangeDNSPtr(ctx, server, ip, dns)
+			},
+		},
+		{
+			name:   "set via load balancer client",
+			apiURL: "/load_balancers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    &dns,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.LoadBalancer.ChangeDNSPtr(ctx, loadBalancer, ip.String(), dns)
+			},
+		},
+		{
+			name:   "reset via load balancer client",
+			apiURL: "/load_balancers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    nil,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.LoadBalancer.ChangeDNSPtr(ctx, loadBalancer, ip.String(), dns)
+			},
+		},
+		{
+			name:   "set load balancer via rdns client",
+			apiURL: "/load_balancers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    &dns,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.RDNS.ChangeDNSPtr(ctx, loadBalancer, ip, dns)
+			},
+		},
+		{
+			name:   "reset load balancer via rdns client",
+			apiURL: "/load_balancers/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    nil,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.RDNS.ChangeDNSPtr(ctx, loadBalancer, ip, dns)
+			},
+		},
+		{
+			name:   "set via floating ip client",
+			apiURL: "/floating_ips/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    &dns,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, ip.String(), dns)
+			},
+		},
+		{
+			name:   "reset via floating ip client",
+			apiURL: "/floating_ips/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    nil,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.FloatingIP.ChangeDNSPtr(ctx, floatingIP, ip.String(), dns)
+			},
+		},
+		{
+			name:   "set floating ip via rdns client",
+			apiURL: "/floating_ips/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    &dns,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.RDNS.ChangeDNSPtr(ctx, floatingIP, ip, dns)
+			},
+		},
+		{
+			name:   "reset floating ip via rdns client",
+			apiURL: "/floating_ips/1/actions/change_dns_ptr",
+			IP:     net.ParseIP("127.0.0.1"),
+			DNS:    nil,
+			changeFunc: func(env *testEnv, ip net.IP, dns *string) (*Action, *Response, error) {
+				return env.Client.RDNS.ChangeDNSPtr(ctx, floatingIP, ip, dns)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			env := newTestEnv()
+			defer env.Teardown()
+
+			env.Mux.HandleFunc(tt.apiURL, func(w http.ResponseWriter, r *http.Request) {
+				var reqBody schema.ServerActionChangeDNSPtrRequest
+				if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
+					t.Fatal(err)
+				}
+				if reqBody.IP != tt.IP.String() {
+					t.Errorf("unexpected IP: %v", reqBody.IP)
+				}
+				if !(reqBody.DNSPtr == tt.DNS || *reqBody.DNSPtr == *tt.DNS) {
+					t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
+				}
+				json.NewEncoder(w).Encode(schema.ServerActionChangeDNSPtrResponse{
+					Action: schema.Action{
+						ID: 1,
+					},
+				})
+			})
+
+			action, _, err := tt.changeFunc(&env, tt.IP, tt.DNS)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if action.ID != 1 {
+				t.Errorf("unexpected action ID: %d", action.ID)
+			}
+		})
+	}
+}
+
+func TestDNSPtrFromIP(t *testing.T) {
+	var (
+		server = Server{
+			ID: 1,
+			PublicNet: ServerPublicNet{
+				IPv4: ServerPublicNetIPv4{
+					IP:     net.ParseIP("127.0.0.1"),
+					DNSPtr: "ipv4.example.com",
+				},
+				IPv6: ServerPublicNetIPv6{
+					DNSPtr: map[string]string{
+						"::1": "ipv6.example.com",
+					},
+				},
+			},
+		}
+		loadBalancer = LoadBalancer{
+			ID: 1,
+			PublicNet: LoadBalancerPublicNet{
+				IPv4: LoadBalancerPublicNetIPv4{
+					IP:     net.ParseIP("127.0.0.1"),
+					DNSPtr: "ipv4.example.com",
+				},
+				IPv6: LoadBalancerPublicNetIPv6{
+					IP:     net.ParseIP("::1"),
+					DNSPtr: "ipv6.example.com",
+				},
+			},
+		}
+		floatingIPv4 = FloatingIP{
+			ID: 1,
+			IP: net.ParseIP("127.0.0.1"),
+			DNSPtr: map[string]string{
+				"127.0.0.1": "ipv4.example.com",
+			},
+		}
+		floatintIPv6 = FloatingIP{
+			ID: 1,
+			IP: net.ParseIP("::1"),
+			DNSPtr: map[string]string{
+				"::1": "ipv6.example.com",
+			},
+		}
+	)
+
+	tests := []struct {
+		name string
+		IP   net.IP
+		DNS  string
+		rdns RDNSSupporter
+	}{
+		{
+			name: "server get dns ptr of IPv4",
+			IP:   net.ParseIP("127.0.0.1"),
+			DNS:  "ipv4.example.com",
+			rdns: &server,
+		},
+		{
+			name: "server get dns ptr of IPv6",
+			IP:   net.ParseIP("::1"),
+			DNS:  "ipv6.example.com",
+			rdns: &server,
+		},
+		{
+			name: "load balancer get dns ptr of IPv4",
+			IP:   net.ParseIP("127.0.0.1"),
+			DNS:  "ipv4.example.com",
+			rdns: &loadBalancer,
+		},
+		{
+			name: "load balancer get dns ptr of IPv6",
+			IP:   net.ParseIP("::1"),
+			DNS:  "ipv6.example.com",
+			rdns: &loadBalancer,
+		},
+		{
+			name: "floating ip get dns ptr of IPv4",
+			IP:   net.ParseIP("127.0.0.1"),
+			DNS:  "ipv4.example.com",
+			rdns: &floatingIPv4,
+		},
+		{
+			name: "floating ip get dns ptr of IPv6",
+			IP:   net.ParseIP("::1"),
+			DNS:  "ipv6.example.com",
+			rdns: &floatintIPv6,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			receivedDNS, err := tt.rdns.GetDNSPtrForIP(tt.IP)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if tt.DNS != receivedDNS {
+				t.Errorf("unexpected dns for ip %s: %s", tt.IP.String(), receivedDNS)
+			}
+		})
+	}
+}
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index f22e500..a6463ff 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -1524,75 +1524,6 @@ func TestServerClientChangeType(t *testing.T) {
 	})
 }
 
-func TestServerClientChangeDNSPtr(t *testing.T) {
-	var (
-		ctx    = context.Background()
-		server = &Server{ID: 1}
-	)
-
-	t.Run("set", func(t *testing.T) {
-		env := newTestEnv()
-		defer env.Teardown()
-
-		env.Mux.HandleFunc("/servers/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) {
-			var reqBody schema.ServerActionChangeDNSPtrRequest
-			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
-				t.Fatal(err)
-			}
-			if reqBody.IP != "127.0.0.1" {
-				t.Errorf("unexpected IP: %v", reqBody.IP)
-			}
-			if reqBody.DNSPtr == nil || *reqBody.DNSPtr != "example.com" {
-				t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
-			}
-			json.NewEncoder(w).Encode(schema.ServerActionChangeDNSPtrResponse{
-				Action: schema.Action{
-					ID: 1,
-				},
-			})
-		})
-
-		action, _, err := env.Client.Server.ChangeDNSPtr(ctx, server, "127.0.0.1", String("example.com"))
-		if err != nil {
-			t.Fatal(err)
-		}
-		if action.ID != 1 {
-			t.Errorf("unexpected action ID: %d", action.ID)
-		}
-	})
-
-	t.Run("reset", func(t *testing.T) {
-		env := newTestEnv()
-		defer env.Teardown()
-
-		env.Mux.HandleFunc("/servers/1/actions/change_dns_ptr", func(w http.ResponseWriter, r *http.Request) {
-			var reqBody schema.ServerActionChangeDNSPtrRequest
-			if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil {
-				t.Fatal(err)
-			}
-			if reqBody.IP != "127.0.0.1" {
-				t.Errorf("unexpected IP: %v", reqBody.IP)
-			}
-			if reqBody.DNSPtr != nil {
-				t.Errorf("unexpected DNS ptr: %v", reqBody.DNSPtr)
-			}
-			json.NewEncoder(w).Encode(schema.ServerActionChangeDNSPtrResponse{
-				Action: schema.Action{
-					ID: 1,
-				},
-			})
-		})
-
-		action, _, err := env.Client.Server.ChangeDNSPtr(ctx, server, "127.0.0.1", nil)
-		if err != nil {
-			t.Fatal(err)
-		}
-		if action.ID != 1 {
-			t.Errorf("unexpected action ID: %d", action.ID)
-		}
-	})
-}
-
 func TestServerClientChangeProtection(t *testing.T) {
 	var (
 		ctx    = context.Background()