Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nm): Backend implementation to support EAP-TLS + Minor WebUI fixes #4872

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ed3fd45
feat: initial implimentation of tls backend
GregoryIvo Sep 28, 2023
6ecf82f
fix: weird formatter oddity
GregoryIvo Sep 28, 2023
d2a8f4e
fix: removed loggers
GregoryIvo Sep 29, 2023
24c1ad6
refactor: ifPresent if's changed to lamdas
GregoryIvo Sep 29, 2023
0d13fdc
tests: update for new tls changes
GregoryIvo Sep 29, 2023
0528ef6
fix: revert un-needed change
GregoryIvo Oct 1, 2023
1ff935b
feat: added support for multiple keystores
GregoryIvo Oct 2, 2023
d8b1478
tests: removed not needed logging
GregoryIvo Oct 2, 2023
e3ea4fb
fix: improved backend stability, and fixed regex
GregoryIvo Oct 3, 2023
66bb8ed
refactor: changed the way the keystore var is passed
GregoryIvo Oct 3, 2023
930b711
feat: added more type security in webUI
GregoryIvo Oct 3, 2023
0081908
refactor: fix comments enable -> unable
GregoryIvo Oct 4, 2023
a38a52f
refactor: removed to string
GregoryIvo Oct 4, 2023
457e659
refactor: certificate replacement method
GregoryIvo Oct 4, 2023
8d52d36
refactor: removed not needed Exception
GregoryIvo Oct 4, 2023
e6c53e7
refactor: decryptAndConvertCertificates method
GregoryIvo Oct 4, 2023
ab1d76c
refactor: removed extra newline
GregoryIvo Oct 4, 2023
6d51d78
refactor: error message
GregoryIvo Oct 4, 2023
6e963a2
refactor: added more if checks to improve reliability when applying f…
GregoryIvo Oct 4, 2023
023df59
refactor: created a hard copy of the modifiedMap so it is not passed …
GregoryIvo Oct 5, 2023
9a3846f
refactor: removed extra 802-1x in String
GregoryIvo Oct 10, 2023
04280ee
refactor: add specificity to to isCertificate method
GregoryIvo Oct 10, 2023
2a31c6d
refactor: updated String for better readability
GregoryIvo Oct 10, 2023
5150b1e
refactor: add extra safety checks in getTrustedCertificateFromKeystor…
GregoryIvo Oct 10, 2023
820d627
lint: fix whitespace issues
GregoryIvo Oct 10, 2023
c95320e
fix: changed 802-1x -> 802.1x
GregoryIvo Oct 11, 2023
f97bdcd
refactor: removed unnecessary cast to String
GregoryIvo Oct 11, 2023
419a67f
refactor: removed extra curly braces in lamdas
GregoryIvo Oct 11, 2023
748d729
refactor: removed type in <>
GregoryIvo Oct 11, 2023
7579211
tests: added basic enterprise test coverage
GregoryIvo Oct 11, 2023
3ede2ee
test: added method for mocking keystore
GregoryIvo Oct 11, 2023
a9ebe52
fix: added static variable for NM_SECRET_FLAGS_NOT_REQUIRED
GregoryIvo Oct 12, 2023
2ff3a7f
fix: removed generic exception logging, and changed exceptions
GregoryIvo Oct 12, 2023
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
2 changes: 2 additions & 0 deletions kura/org.eclipse.kura.nm/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Import-Package: org.apache.commons.io;version="2.4.0",
org.eclipse.kura.core.net.modem;version="[1.0,2.0)",
org.eclipse.kura.core.net.util;version="[1.0,2.0)",
org.eclipse.kura.core.util;version="[1.0,2.0)",
org.eclipse.kura.core.keystore.util;version="[1.0,2.0)",
org.eclipse.kura.crypto;version="[1.1,2.0)",
org.eclipse.kura.executor;version="[1.0,2.0)",
org.eclipse.kura.internal.linux.net.dns;version="[1.0,2.0)",
Expand All @@ -35,6 +36,7 @@ Import-Package: org.apache.commons.io;version="2.4.0",
org.eclipse.kura.net.status.vlan;version="[1.0,2.0)",
org.eclipse.kura.net.wifi;version="[2.4,3.0]",
org.eclipse.kura.usb;version="[1.0,2.0)",
org.eclipse.kura.security.keystore;version="[1.0,2.0)",
org.osgi.framework;version="1.5.0",
org.osgi.service.component;version="1.2.0",
org.osgi.service.event;version="1.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@
<reference bind="setExecutorService" cardinality="1..1" interface="org.eclipse.kura.executor.PrivilegedExecutorService" name="PrivilegedExecutorService" policy="static" unbind="unsetExecutorService"/>
<reference bind="setDnsServerService" cardinality="1..1" interface="org.eclipse.kura.internal.linux.net.dns.DnsServerService" name="DNSService" policy="static" />
<reference name="CryptoService" interface="org.eclipse.kura.crypto.CryptoService" bind="setCryptoService" unbind="unsetCryptoService" cardinality="1..1" policy="static"/>
<reference name="KeystoreService" interface="org.eclipse.kura.security.keystore.KeystoreService" bind="setKeystoreService" unbind="unsetKeystoreService" cardinality="0..n" policy="dynamic"/>
mattdibi marked this conversation as resolved.
Show resolved Hide resolved
</scr:component>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
package org.eclipse.kura.nm.configuration;

import java.net.UnknownHostException;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStore.TrustedCertificateEntry;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -25,9 +29,10 @@
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.kura.KuraErrorCode;
import org.eclipse.kura.KuraException;
import org.eclipse.kura.configuration.ComponentConfiguration;
import org.eclipse.kura.configuration.ConfigurationService;
import org.eclipse.kura.configuration.Password;
import org.eclipse.kura.configuration.SelfConfiguringComponent;
import org.eclipse.kura.crypto.CryptoService;
Expand All @@ -45,6 +50,7 @@
import org.eclipse.kura.nm.configuration.monitor.DnsServerMonitor;
import org.eclipse.kura.nm.configuration.writer.DhcpServerConfigWriter;
import org.eclipse.kura.nm.configuration.writer.FirewallNatConfigWriter;
import org.eclipse.kura.security.keystore.KeystoreService;
import org.freedesktop.dbus.exceptions.DBusException;
import org.freedesktop.dbus.exceptions.DBusExecutionException;
import org.osgi.service.component.ComponentContext;
Expand All @@ -61,19 +67,20 @@ public class NMConfigurationServiceImpl implements SelfConfiguringComponent {
private static final String MODIFIED_INTERFACE_NAMES = "modified.interface.names";
private static final String MODEM_PORT_REGEX = "^\\d+-\\d+";
private static final Pattern PPP_INTERFACE = Pattern.compile("ppp\\d+");

private static final List<NetInterfaceType> SUPPORTED_NAT_INTERFACE_TYPES = Arrays.asList(
NetInterfaceType.ETHERNET, NetInterfaceType.WIFI, NetInterfaceType.MODEM,
NetInterfaceType.VLAN);
private static final List<NetInterfaceType> SUPPORTED_DHCP_SERVER_INTERFACE_TYPES = Arrays.asList(
NetInterfaceType.ETHERNET, NetInterfaceType.WIFI, NetInterfaceType.VLAN);

private static final List<NetInterfaceType> SUPPORTED_NAT_INTERFACE_TYPES = Arrays.asList(NetInterfaceType.ETHERNET,
NetInterfaceType.WIFI, NetInterfaceType.MODEM, NetInterfaceType.VLAN);
private static final List<NetInterfaceType> SUPPORTED_DHCP_SERVER_INTERFACE_TYPES = Arrays
.asList(NetInterfaceType.ETHERNET, NetInterfaceType.WIFI, NetInterfaceType.VLAN);

private NetworkService networkService;
private DnsServerService dnsServer;
private EventAdmin eventAdmin;
private CommandExecutorService commandExecutorService;
private CryptoService cryptoService;

private Map<String, KeystoreService> keystoreServices = new HashMap<>();

private DhcpServerMonitor dhcpServerMonitor;
private DnsServerMonitor dnsServerMonitor;

Expand Down Expand Up @@ -127,6 +134,16 @@ public void unsetCryptoService(CryptoService cryptoService) {
}
}

public void setKeystoreService(KeystoreService keystoreService, Map<String, Object> properties) {
this.keystoreServices.put((String) properties.get(ConfigurationService.KURA_SERVICE_PID), keystoreService);
}

public void unsetKeystoreService(KeystoreService keystoreService, Map<String, Object> properties) {
if (this.keystoreServices.containsValue(keystoreService)) {
this.keystoreServices.remove(properties.get(ConfigurationService.KURA_SERVICE_PID));
}
}

public void setDnsServerService(DnsServerService dnsServer) {
this.dnsServer = dnsServer;
}
Expand Down Expand Up @@ -213,13 +230,16 @@ public synchronized void update(Map<String, Object> receivedProperties) {
}
if (NetInterfaceType.MODEM.equals(interfaceTypeProperty.get())) {
setModemPppNumber(modifiedProps, interfaceName);
}
}
}

mergeNetworkConfigurationProperties(modifiedProps, this.networkProperties.getProperties());

this.networkProperties = new NetworkProperties(
discardModifiedNetworkInterfaces(new HashMap<>(modifiedProps)));

decryptAndConvertPasswordProperties(modifiedProps);
this.networkProperties = new NetworkProperties(discardModifiedNetworkInterfaces(modifiedProps));
decryptAndConvertCertificatesProperties(modifiedProps, interfaces);

writeNetworkConfigurationSettings(modifiedProps);
writeFirewallNatRules(interfaces, modifiedProps);
Expand Down Expand Up @@ -252,7 +272,7 @@ protected void setModemPppNumber(Map<String, Object> modifiedProps, String inter
Integer pppNum = Integer.valueOf(this.networkService.getModemPppInterfaceName(interfaceName).substring(3));
modifiedProps.put(String.format(PREFIX + "%s.config.pppNum", interfaceName), pppNum);
}

protected void setInterfaceType(Map<String, Object> modifiedProps, String interfaceName, NetInterfaceType type) {
modifiedProps.put(String.format(PREFIX + "%s.type", interfaceName), type.toString());
}
Expand Down Expand Up @@ -295,6 +315,86 @@ private void decryptAndConvertPasswordProperties(Map<String, Object> modifiedPro
}
}

private void decryptAndConvertCertificatesProperties(Map<String, Object> modifiedProps, Set<String> interfaces) {
Copy link
Contributor

@mattdibi mattdibi Oct 12, 2023

Choose a reason for hiding this comment

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

One final thought: this is a Side Effect Method.

A side effect method is a method which modifies some state variable value/arguments passed having a consequence beyond its scope, that is to say it has an observable effect besides returning a value (the main effect) to the invoker of the operation.

which is a known anti-pattern/bad practice (see Uncle Bob Martin's "Clean Code", there's a dedicated section for that).

The correct way to handle this would be to return a modified version of the passed argument, but we already had the decryptAndConvertPasswordProperties that behaved the same way and therefore we're doing the same for consistency.

Again: leave it as-is but be aware of the fact that this is not the best implementation.

Also notice that you've hidden the actual modification of the map down a couple of layer of abstractions (you actually change the values inside findAndDecodeCertificatesForInterface), so you've hidden the Side Effect even more than what was done in decryptAndConvertPasswordProperties.

Regarding why this is considered a bad practice... you experienced it first hand while chasing that bug caused by the modified map 😆


interfaces.forEach(interfaceName -> {
String key = String.format("net.interface.%s.config.802-1x.keystore.pid", interfaceName);
if (modifiedProps.containsKey(key)) {

Object prop = modifiedProps.get(key);

if (prop instanceof String) {
String keystorePid = (String) prop;

findAndDecodeCertificatesForInterface(interfaceName, modifiedProps,
this.keystoreServices.get(keystorePid));
}
}
Comment on lines +322 to +332
Copy link
Contributor

Choose a reason for hiding this comment

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

You can leave the code as-is but consider using Early Returns in the future. I think the code is much easier to read:

Suggested change
if (modifiedProps.containsKey(key)) {
Object prop = modifiedProps.get(key);
if (prop instanceof String) {
String keystorePid = (String) prop;
findAndDecodeCertificatesForInterface(interfaceName, modifiedProps,
this.keystoreServices.get(keystorePid));
}
}
if (!modifiedProps.containsKey(key)) {
return; // we cannot "continue" since we're inside a lambda
}
Object keystorePid = modifiedProps.get(key);
if (!keystorePid instanceof String) {
return;
}
findAndDecodeCertificatesForInterface(interfaceName, modifiedProps,
this.keystoreServices.get((String) keystorePid));

});
}

private void findAndDecodeCertificatesForInterface(String interfaceName, Map<String, Object> modifiedProps,
mattdibi marked this conversation as resolved.
Show resolved Hide resolved
KeystoreService keystoreService) {

if (keystoreService == null) {
logger.error("Cannot find keystore service for interface {}", interfaceName);
return;
}

final String clientCertString = String.format("net.interface.%s.config.802-1x.client-cert-name", interfaceName);
final String caCertString = String.format("net.interface.%s.config.802-1x.ca-cert-name", interfaceName);
final String privateKeyString = String.format("net.interface.%s.config.802-1x.private-key-name", interfaceName);
final List<String> keyCertStrings = Arrays.asList(clientCertString, caCertString, privateKeyString);

for (String key : keyCertStrings) {
if (!modifiedProps.containsKey(key)) {
continue;
}

Object value = modifiedProps.get(key);
try {
String valueString = value.toString();
if (isCertificate(key)) {
modifiedProps.put(key, getTrustedCertificateFromKeystore(valueString, keystoreService));
} else {
modifiedProps.put(key, getTrustedPrivateKeyFromKeystore(valueString, keystoreService));
}
} catch (KuraException e) {
logger.error("Unable to decode key/certificate {} from keystore.", key, e);
modifiedProps.put(key, value);
}
}
}

private boolean isCertificate(String key) {
return key.contains("802-1x.client-cert-name") || key.contains("802-1x.ca-cert-name");
}

private Certificate getTrustedCertificateFromKeystore(String certificateName, KeystoreService keystoreService)
throws KuraException {
if (keystoreService.getEntry(certificateName) instanceof TrustedCertificateEntry) {
TrustedCertificateEntry cert = (TrustedCertificateEntry) keystoreService.getEntry(certificateName);
return cert.getTrustedCertificate();
} else if (keystoreService.getEntry(certificateName) instanceof PrivateKeyEntry) {
PrivateKeyEntry cert = (PrivateKeyEntry) keystoreService.getEntry(certificateName);
return cert.getCertificate();
} else {
throw new KuraException(KuraErrorCode.CONFIGURATION_ERROR,
String.format("Certificate \"%s\" is not of the expected key type or not found.", certificateName));
}
}

private PrivateKey getTrustedPrivateKeyFromKeystore(String privateKeyName, KeystoreService keystoreService)
throws KuraException {
if (!(keystoreService.getEntry(privateKeyName) instanceof PrivateKeyEntry)) {
throw new KuraException(KuraErrorCode.CONFIGURATION_ERROR,
String.format("Private key \"%s\" is not of the expected key type or not found.", privateKeyName));
}

PrivateKeyEntry key = (PrivateKeyEntry) keystoreService.getEntry(privateKeyName);
return key.getPrivateKey();
}

@Override
@SuppressWarnings("restriction")
public synchronized ComponentConfiguration getConfiguration() throws KuraException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -64,6 +67,8 @@ public class NMSettingsConverter {
private static final String KURA_PROPS_KEY_WIFI_MODE = "net.interface.%s.config.wifi.mode";
private static final String KURA_PROPS_KEY_WIFI_SECURITY_TYPE = "net.interface.%s.config.wifi.%s.securityType";

static final UInt32 NM_SECRET_FLAGS_NOT_REQUIRED = new UInt32(4);

private NMSettingsConverter() {
throw new IllegalStateException("Utility class");
}
Expand Down Expand Up @@ -171,36 +176,51 @@ private static void create8021xTls(NetworkProperties props, String deviceId, Map
String identity = props.get(String.class, "net.interface.%s.config.802-1x.identity", deviceId);
settings.put("identity", new Variant<>(identity));

String clientCert = props.get(String.class, "net.interface.%s.config.802-1x.client-cert", deviceId);
settings.put("client-cert", new Variant<>(clientCert.getBytes(StandardCharsets.UTF_8)));
try {
Certificate clientCert = props.get(Certificate.class, "net.interface.%s.config.802-1x.client-cert-name",
deviceId);
settings.put("client-cert", new Variant<>(clientCert.getEncoded()));
} catch (CertificateEncodingException e) {
logger.error("Unable to find or decode Client Certificate");
}

PrivateKey privateKey = props.get(PrivateKey.class, "net.interface.%s.config.802-1x.private-key-name",
deviceId);

if (privateKey.getEncoded() != null) {
settings.put("private-key", new Variant<>(privateKey.getEncoded()));
} else {
logger.error("Unable to find or decode Private Key");
}

Optional<Password> privateKeyPassword = props.getOpt(Password.class,
"net.interface.%s.config.802-1x.private-key-password", deviceId);

String privateKey = props.get(String.class, "net.interface.%s.config.802-1x.private-key", deviceId);
settings.put("private-key", new Variant<>(privateKey.getBytes(StandardCharsets.UTF_8)));
privateKeyPassword.ifPresent(value -> settings.put("private-key-password", new Variant<>(value.toString())));

String privateKeyPassword = props
.get(Password.class, "net.interface.%s.config.802-1x.private-key-password", deviceId).toString();
settings.put("private-key-password", new Variant<>(privateKeyPassword));
settings.put("private-key-password-flags", new Variant<>(NM_SECRET_FLAGS_NOT_REQUIRED));

}

private static void create8021xOptionalCaCertAndAnonIdentity(NetworkProperties props, String deviceId,
Map<String, Variant<?>> settings) {

Optional<String> anonymousIdentity = props.getOpt(String.class,
"net.interface.%s.config.802-1x.anonymous-identity", deviceId);
if (anonymousIdentity.isPresent()) {
settings.put("anonymous-identity", new Variant<>(anonymousIdentity.get()));
}

Optional<String> caCert = props.getOpt(String.class, "net.interface.%s.config.802-1x.ca-cert", deviceId);
if (caCert.isPresent()) {
settings.put("ca-cert", new Variant<>(caCert.get().getBytes(StandardCharsets.UTF_8)));
anonymousIdentity.ifPresent(value -> settings.put("anonymous-identity", new Variant<>(value)));

try {
Certificate caCert = props.get(Certificate.class, "net.interface.%s.config.802-1x.ca-cert-name", deviceId);
GregoryIvo marked this conversation as resolved.
Show resolved Hide resolved
settings.put("ca-cert", new Variant<>(caCert.getEncoded()));
} catch (Exception e) {
logger.error(String.format("Unable to find or decode CA Certificate for interface %s", deviceId));
}

Optional<Password> caCertPassword = props.getOpt(Password.class,
"net.interface.%s.config.802-1x.ca-cert-password", deviceId);
if (caCertPassword.isPresent()) {
settings.put("ca-cert-password", new Variant<>(caCertPassword.get().toString()));
}

caCertPassword.ifPresent(value -> settings.put("ca-cert-password", new Variant<>(value.toString())));
}

private static void create8021xMschapV2(NetworkProperties props, String deviceId,
Expand Down Expand Up @@ -512,7 +532,7 @@ public static Map<String, Variant<?>> buildPPPSettings(NetworkProperties props,

return settings;
}

public static Map<String, Variant<?>> buildVlanSettings(NetworkProperties props, String deviceId) {
Map<String, Variant<?>> settings = new HashMap<>();
settings.put("interface-name", new Variant<>(deviceId));
Expand All @@ -524,11 +544,9 @@ public static Map<String, Variant<?>> buildVlanSettings(NetworkProperties props,
settings.put("flags", new Variant<>(new UInt32(vlanFlags.orElse(1))));
DBusListType listType = new DBusListType(String.class);
Optional<List<String>> ingressMap = props.getOptStringList("net.interface.%s.config.vlan.ingress", deviceId);
settings.put("ingress-priority-map", new Variant<>(ingressMap
.orElse(new ArrayList<String>()), listType));
settings.put("ingress-priority-map", new Variant<>(ingressMap.orElse(new ArrayList<>()), listType));
Optional<List<String>> egressMap = props.getOptStringList("net.interface.%s.config.vlan.egress", deviceId);
settings.put("egress-priority-map", new Variant<>(egressMap
.orElse(new ArrayList<String>()), listType));
settings.put("egress-priority-map", new Variant<>(egressMap.orElse(new ArrayList<>()), listType));
return settings;
}

Expand All @@ -552,7 +570,7 @@ public static Map<String, Variant<?>> buildConnectionSettings(Optional<Connectio

return connectionMap;
}

private static Map<String, Variant<?>> createConnectionSettings(String iface) {
Map<String, Variant<?>> connectionMap = new HashMap<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private void monitor() {
stopDhcpServer(interfaceName);
}
} catch (KuraException e) {
logger.warn("Failed to chech DHCP server status for the interface " + interfaceName, e);
logger.warn("Failed to check DHCP server status for the interface " + interfaceName, e);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,16 +327,21 @@ public void setDirty(boolean dirty) {
public void getUpdatedNetInterface(GwtNetInterfaceConfig updatedNetIf) {
Gwt8021xConfig updated8021xConfig = new Gwt8021xConfig();

if (this.username.isEnabled()) {
if (!this.username.getText().isEmpty() && this.username.getText() != null) {
updated8021xConfig.setIdentity(this.username.getText());
}

if (this.password.isEnabled()) {
if (!this.password.getText().isEmpty() && this.password.getText() != null) {
updated8021xConfig.setPassword(this.password.getText());
}

updated8021xConfig.setEap(Gwt8021xEap.valueOf(this.eap.getSelectedValue()));
updated8021xConfig.setInnerAuthEnum(Gwt8021xInnerAuth.valueOf(this.innerAuth.getSelectedValue()));
if (!this.eap.getSelectedValue().isEmpty() && this.eap.getSelectedValue() != null) {
updated8021xConfig.setEap(Gwt8021xEap.valueOf(this.eap.getSelectedValue()));
}

if (!this.innerAuth.getSelectedValue().isEmpty() && this.innerAuth.getSelectedValue() != null) {
updated8021xConfig.setInnerAuthEnum(Gwt8021xInnerAuth.valueOf(this.innerAuth.getSelectedValue()));
}

updatedNetIf.setEnterpriseConfig(updated8021xConfig);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ private void refreshForm() {
this.dns.setEnabled(false);
}
this.renew.setEnabled(false);

this.configure.setSelectedIndex(this.configure.getItemText(0).equals(IPV4_MODE_DHCP_MESSAGE) ? 0 : 1);
} else if (this.selectedNetIfConfig != null
&& this.selectedNetIfConfig.getHwTypeEnum() == GwtNetIfType.LOOPBACK) {
Expand Down Expand Up @@ -867,4 +868,4 @@ private void initModal() {
this.multipleWanWarnText.setText(MSGS.netStatusWarning());
this.wanModal.addHideHandler(evt -> this.setDirty(true));
}
}
}
Loading