Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
import com.cloud.user.dao.AccountDao;
import com.cloud.utils.Pair;
import com.cloud.utils.StringUtils;
import com.cloud.utils.UriUtils;
import com.cloud.utils.net.NetUtils;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.component.PluggableService;
import com.cloud.utils.db.Filter;
Expand All @@ -107,9 +109,7 @@
import com.cloud.vm.Nic;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.NicDao;
import com.cloud.vm.dao.NicDetailsDao;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDao;

@Component
Expand All @@ -126,10 +126,6 @@
@Inject
DnsZoneNetworkMapDao dnsZoneNetworkMapDao;
@Inject
UserVmDao userVmDao;
@Inject
NicDao nicDao;
@Inject
DomainDao domainDao;
@Inject
DnsZoneJoinDao dnsZoneJoinDao;
Expand Down Expand Up @@ -162,14 +158,41 @@
throw new CloudRuntimeException("No plugin found for DNS provider type: " + type);
}

/**
* Trims and rejects a DNS provider URL that resolves to an illegal address before any provider client
* is given the chance to connect to it. See {@link UriUtils#validateUrl(String)} for the exact rules
* enforced (including the requirement that the URL declares an {@code http}/{@code https} scheme).
* Private/site-local addresses (e.g. {@code 192.168.0.0/16}) are only permitted for root admin callers.
*
* @throws InvalidParameterValueException if the URL is blank, fails validation, or is a private address
* requested by a non-root-admin caller.
*/
private void validateDnsServerUrl(String trimmedUrl, Account caller) {
if (StringUtils.isBlank(trimmedUrl)) {

Check failure on line 171 in server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use static access with "org.apache.commons.lang3.StringUtils" for "isBlank".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaAEkOQUYEZf7TajSZpo&open=AaAEkOQUYEZf7TajSZpo&pullRequest=13821
throw new InvalidParameterValueException("URL cannot be blank.");
}
Pair<String, Integer> hostAndPort;
try {
hostAndPort = UriUtils.validateUrl(trimmedUrl);
} catch (IllegalArgumentException e) {
throw new InvalidParameterValueException(e.getMessage());
}
if (!accountMgr.isRootAdmin(caller.getId()) && NetUtils.isSiteLocalAddress(hostAndPort.first())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!accountMgr.isRootAdmin(caller.getId()) && NetUtils.isSiteLocalAddress(hostAndPort.first())) {
if (NetUtils.isSiteLocalAddress(hostAndPort.first()) && !accountMgr.isRootAdmin(caller.getId())) {

I would invert these two because isSiteLocalAddress is a simpler operation that does not query the database

throw new InvalidParameterValueException(
"Only root admin accounts can configure a DNS server on a private/internal network address.");
}
Comment thread
DaanHoogland marked this conversation as resolved.
Comment on lines +175 to +183

@winterhazel winterhazel Aug 19, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sudo87 this suggestion by Copilot seems to make sense

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@winterhazel sure, will try to address this.

@sudo87 sudo87 Aug 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@winterhazel There are few options:

  1. Only allow root admin to perform add/update/delete DNS server operations. This will be sufficient to prevent it
  2. Restrict non-root users to provide IP for DNS registration and it will enable users to setup their dns server
  3. We resolve ip for the url for each transaction and ip must adhere to the restrictions placed for non-root users. We can add some global settings to define those. This will introduce additional latency for each request.

We prefer 2 option
cc: @weizhouapache

}
Comment thread
winterhazel marked this conversation as resolved.

@Override
@ActionEvent(eventType = EventTypes.EVENT_DNS_SERVER_ADD, eventDescription = "Adding a DNS Server")
public DnsServer addDnsServer(AddDnsServerCmd cmd) {
Account caller = CallContext.current().getCallingAccount();
DnsServer existing = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), caller.getId());
String dnsUrl = StringUtils.trim(cmd.getUrl());

Check failure on line 190 in server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use static access with "org.apache.commons.lang3.StringUtils" for "trim".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaAVNSjxBuamWNefnd6q&open=AaAVNSjxBuamWNefnd6q&pullRequest=13821
validateDnsServerUrl(dnsUrl, caller);
DnsServer existing = dnsServerDao.findByUrlAndAccount(dnsUrl, caller.getId());
if (existing != null) {
throw new InvalidParameterValueException(
"This Account already has a DNS server integration for URL: " + cmd.getUrl());
"This Account already has a DNS server integration for URL: " + dnsUrl);
}

boolean isDnsPublic = cmd.isPublic();
Expand All @@ -185,7 +208,7 @@
}

DnsProviderType type = cmd.getProvider();
DnsServerVO server = new DnsServerVO(cmd.getName(), cmd.getUrl(), cmd.getPort(), type,
DnsServerVO server = new DnsServerVO(cmd.getName(), dnsUrl, cmd.getPort(), type,
cmd.getDnsUserName(), cmd.getDnsApiKey(), isDnsPublic, publicDomainSuffix, cmd.getNameServers(),
caller.getAccountId(), caller.getDomainId());

Expand Down Expand Up @@ -251,12 +274,14 @@
}

if (cmd.getUrl() != null) {
if (!cmd.getUrl().equals(originalUrl)) {
DnsServer duplicate = dnsServerDao.findByUrlAndAccount(cmd.getUrl(), dnsServer.getAccountId());
String dnsUrl = StringUtils.trim(cmd.getUrl());

Check failure on line 277 in server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use static access with "org.apache.commons.lang3.StringUtils" for "trim".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaAVNSjxBuamWNefnd6r&open=AaAVNSjxBuamWNefnd6r&pullRequest=13821
if (!dnsUrl.equals(originalUrl)) {
validateDnsServerUrl(dnsUrl, caller);
DnsServer duplicate = dnsServerDao.findByUrlAndAccount(dnsUrl, dnsServer.getAccountId());
if (duplicate != null && duplicate.getId() != dnsServer.getId()) {
throw new InvalidParameterValueException("Another DNS server with this URL already exists.");
}
dnsServer.setUrl(cmd.getUrl());
dnsServer.setUrl(dnsUrl);
validationRequired = true;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getUrl()).thenReturn("http://192.0.2.1:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null);
when(dnsProviderMock.validateAndResolveServer(any())).thenReturn("resolved-id");
Expand Down Expand Up @@ -781,18 +781,76 @@
public void testAddDnsServerAlreadyExists() {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getUrl()).thenReturn("http://192.0.2.1:8081");
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(serverVO);
manager.addDnsServer(cmd);
}

@Test
public void testAddDnsServerTrimsUrlBeforeDuplicateCheckAndPersistence() throws Exception {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getUrl()).thenReturn(" http://192.0.2.1:8081 ");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null);
when(dnsProviderMock.validateAndResolveServer(any())).thenReturn("resolved-id");
when(dnsServerDao.persist(any())).thenReturn(serverVO);

manager.addDnsServer(cmd);

verify(dnsServerDao).findByUrlAndAccount(eq("http://192.0.2.1:8081"), anyLong());
verify(dnsServerDao).persist(Mockito.argThat(s -> "http://192.0.2.1:8081".equals(((DnsServerVO) s).getUrl())));
}

@Test(expected = InvalidParameterValueException.class)
public void testAddDnsServerRejectsLoopbackUrl() {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(cmd.getUrl()).thenReturn("http://127.0.0.1:8081");
manager.addDnsServer(cmd);
}

@Test(expected = InvalidParameterValueException.class)
public void testAddDnsServerRejectsUrlWithoutScheme() {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(cmd.getUrl()).thenReturn("192.0.2.1:8081");
manager.addDnsServer(cmd);
}

@Test(expected = InvalidParameterValueException.class)
public void testAddDnsServerRejectsPrivateAddressForNonRootAdmin() {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(false);
when(cmd.getUrl()).thenReturn("http://192.168.1.1:8081");
manager.addDnsServer(cmd);
}

@Test
public void testAddDnsServerAllowsPrivateAddressForRootAdmin() throws Exception {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getUrl()).thenReturn("http://192.168.1.1:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null);
when(dnsProviderMock.validateAndResolveServer(any())).thenReturn("resolved-id");
when(dnsServerDao.persist(any())).thenReturn(serverVO);

DnsServer result = manager.addDnsServer(cmd);
assertNotNull(result);
verify(dnsServerDao).persist(any());
}

@Test
public void testAddDnsServerNormalUser() throws Exception {
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(false);
when(accountMgr.isDomainAdmin(callerMock.getId())).thenReturn(false);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getUrl()).thenReturn("http://192.0.2.1:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(cmd.getNameServers()).thenReturn(Collections.emptyList());
when(cmd.isPublic()).thenReturn(true);
Expand All @@ -811,7 +869,7 @@
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.AddDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getUrl()).thenReturn("http://newpdns:8081");
when(cmd.getUrl()).thenReturn("http://192.0.2.1:8081");
when(cmd.getProvider()).thenReturn(DnsProviderType.PowerDNS);
when(cmd.getNameServers()).thenReturn(Collections.emptyList());
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null);
Expand All @@ -824,7 +882,7 @@
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class);
when(cmd.getId()).thenReturn(SERVER_ID);
when(cmd.getUrl()).thenReturn("http://duplicate:8081");
when(cmd.getUrl()).thenReturn("http://192.0.2.1:8081");
DnsServerVO existingServer = mock(DnsServerVO.class);
when(existingServer.getId()).thenReturn(SERVER_ID + 1); // Different ID implies duplicate

Expand All @@ -835,12 +893,74 @@
manager.updateDnsServer(cmd);
}

@Test(expected = InvalidParameterValueException.class)
public void testUpdateDnsServerRejectsLoopbackUrl() {
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class);
when(cmd.getId()).thenReturn(SERVER_ID);
when(cmd.getUrl()).thenReturn("http://127.0.0.1:8081");
when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
Mockito.doReturn("http://original:8081").when(serverVO).getUrl();

Check warning on line 903 in server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "doReturn".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AZ_b59q_GDdNQ584wbmu&open=AZ_b59q_GDdNQ584wbmu&pullRequest=13821

manager.updateDnsServer(cmd);
}

@Test(expected = InvalidParameterValueException.class)
public void testUpdateDnsServerRejectsPrivateAddressForNonRootAdmin() {
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(false);
when(cmd.getId()).thenReturn(SERVER_ID);
when(cmd.getUrl()).thenReturn("http://192.168.1.1:8081");
when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
Mockito.doReturn("http://original:8081").when(serverVO).getUrl();

Check warning on line 916 in server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "doReturn".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaAUhlxVXt_QL6VJMwC3&open=AaAUhlxVXt_QL6VJMwC3&pullRequest=13821

manager.updateDnsServer(cmd);
}

@Test
public void testUpdateDnsServerAllowsPrivateAddressForRootAdmin() throws Exception {
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class);
when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(true);
when(cmd.getId()).thenReturn(SERVER_ID);
when(cmd.getUrl()).thenReturn("http://192.168.1.1:8081");
when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
Mockito.doReturn("http://original:8081").when(serverVO).getUrl();

Check warning on line 929 in server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "doReturn".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaAUhlxVXt_QL6VJMwC4&open=AaAUhlxVXt_QL6VJMwC4&pullRequest=13821
Mockito.doReturn(DnsProviderType.PowerDNS).when(serverVO).getProviderType();

Check warning on line 930 in server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "doReturn".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AaAUhlxVXt_QL6VJMwC5&open=AaAUhlxVXt_QL6VJMwC5&pullRequest=13821
when(dnsServerDao.findByUrlAndAccount(anyString(), anyLong())).thenReturn(null);
doNothing().when(dnsProviderMock).validate(any());
when(dnsServerDao.update(anyLong(), any())).thenReturn(true);

DnsServer result = manager.updateDnsServer(cmd);
assertNotNull(result);
verify(dnsProviderMock).validate(any());
}

@Test
public void testUpdateDnsServerTreatsWhitespaceOnlyUrlChangeAsUnchanged() throws Exception {
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class);
Integer unchangedPort = serverVO.getPort();
when(cmd.getId()).thenReturn(SERVER_ID);
when(cmd.getUrl()).thenReturn(" http://192.0.2.1:8081 ");
when(cmd.getPort()).thenReturn(unchangedPort);
when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
Mockito.doReturn("http://192.0.2.1:8081").when(serverVO).getUrl();

Check warning on line 949 in server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use a static import for "doReturn".

See more on https://sonarcloud.io/project/issues?id=apache_cloudstack&issues=AZ_b59q_GDdNQ584wbmv&open=AZ_b59q_GDdNQ584wbmv&pullRequest=13821
when(dnsServerDao.update(anyLong(), any())).thenReturn(true);

DnsServer result = manager.updateDnsServer(cmd);
assertNotNull(result);
verify(dnsProviderMock, never()).validate(any());
verify(serverVO, never()).setUrl(anyString());
}

@Test
public void testUpdateDnsServerUrlValid() throws Exception {
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd cmd = mock(
org.apache.cloudstack.api.command.user.dns.UpdateDnsServerCmd.class);
when(cmd.getId()).thenReturn(SERVER_ID);
when(cmd.getUrl()).thenReturn("http://new-url:8081");
when(cmd.getUrl()).thenReturn("http://192.0.2.1:8081");
when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);

Mockito.doReturn("http://original:8081").when(serverVO).getUrl();
Expand Down
Loading