001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.common.networking; 019 020import java.net.NetworkInterface; 021import java.nio.file.Files; 022import java.nio.file.Path; 023import java.nio.file.Paths; 024import java.util.HashMap; 025import java.util.NoSuchElementException; 026import org.photonvision.common.configuration.ConfigManager; 027import org.photonvision.common.configuration.NetworkConfig; 028import org.photonvision.common.dataflow.DataChangeDestination; 029import org.photonvision.common.dataflow.DataChangeService; 030import org.photonvision.common.dataflow.DataChangeSource; 031import org.photonvision.common.dataflow.events.DataChangeEvent; 032import org.photonvision.common.hardware.Platform; 033import org.photonvision.common.hardware.PlatformUtils; 034import org.photonvision.common.logging.LogGroup; 035import org.photonvision.common.logging.Logger; 036import org.photonvision.common.networking.NetworkUtils.NMDeviceInfo; 037import org.photonvision.common.util.ShellExec; 038import org.photonvision.common.util.TimedTaskManager; 039 040public class NetworkManager { 041 private static final Logger logger = new Logger(NetworkManager.class, LogGroup.General); 042 private HashMap<String, String> activeConnections = new HashMap<String, String>(); 043 044 private NetworkManager() {} 045 046 private static class SingletonHolder { 047 private static final NetworkManager INSTANCE = new NetworkManager(); 048 } 049 050 public static NetworkManager getInstance() { 051 return SingletonHolder.INSTANCE; 052 } 053 054 private boolean isManaged = false; 055 public boolean networkingIsDisabled = false; // Passed in via CLI 056 057 public void initialize(boolean shouldManage) { 058 isManaged = shouldManage && !networkingIsDisabled; 059 if (!isManaged) { 060 logger.info("Network management is disabled."); 061 return; 062 } 063 064 if (!Platform.isLinux()) { 065 logger.info("Not managing network on non-Linux platforms."); 066 this.networkingIsDisabled = true; 067 return; 068 } 069 070 if (!PlatformUtils.isRoot()) { 071 logger.error("Cannot manage network without root!"); 072 this.networkingIsDisabled = true; 073 return; 074 } 075 076 // Start tasks to monitor the network interface(s) 077 var ethernetDevices = NetworkUtils.getAllWiredInterfaces(); 078 for (NMDeviceInfo deviceInfo : ethernetDevices) { 079 activeConnections.put( 080 deviceInfo.devName, NetworkUtils.getActiveConnection(deviceInfo.devName)); 081 monitorDevice(deviceInfo.devName, 5000); 082 } 083 084 var physicalDevices = NetworkUtils.getAllActiveWiredInterfaces(); 085 var config = ConfigManager.getInstance().getConfig().getNetworkConfig(); 086 if (physicalDevices.stream().noneMatch(it -> (it.devName.equals(config.networkManagerIface)))) { 087 try { 088 // if the configured interface isn't in the list of available ones, select one that is 089 var iFace = physicalDevices.stream().findFirst().orElseThrow(); 090 logger.warn( 091 "The configured interface doesn't match any available interface. Applying configuration to " 092 + iFace.devName); 093 // update NetworkConfig with found interface 094 config.networkManagerIface = iFace.devName; 095 ConfigManager.getInstance().requestSave(); 096 } catch (NoSuchElementException e) { 097 // if there are no available interfaces, go with the one from settings 098 logger.warn("No physical interface found. Maybe ethernet isn't connected?"); 099 if (config.networkManagerIface == null || config.networkManagerIface.isBlank()) { 100 // if it's also empty, there is nothing to configure 101 logger.error("No valid network interfaces to manage"); 102 return; 103 } 104 } 105 } 106 107 logger.info( 108 "Setting " 109 + config.connectionType 110 + " with team " 111 + config.ntServerAddress 112 + " on " 113 + config.networkManagerIface); 114 115 // always set hostname (unless it's blank) 116 if (!config.hostname.isBlank()) { 117 setHostname(config.hostname); 118 } else { 119 logger.warn("Got empty hostname?"); 120 } 121 122 if (config.connectionType == NetworkMode.DHCP) { 123 setConnectionDHCP(config); 124 } else if (config.connectionType == NetworkMode.STATIC) { 125 setConnectionStatic(config); 126 } 127 } 128 129 public void reinitialize() { 130 initialize(ConfigManager.getInstance().getConfig().getNetworkConfig().shouldManage); 131 132 DataChangeService.getInstance() 133 .publishEvent( 134 new DataChangeEvent<Boolean>( 135 DataChangeSource.DCS_OTHER, 136 DataChangeDestination.DCD_WEBSERVER, 137 "restartServer", 138 true)); 139 } 140 141 private void setHostname(String hostname) { 142 try { 143 var shell = new ShellExec(true, false); 144 shell.executeBashCommand("cat /etc/hostname | tr -d \" \\t\\n\\r\""); 145 var oldHostname = shell.getOutput().replace("\n", ""); 146 logger.debug("Old host name: \"" + oldHostname + "\""); 147 logger.debug("New host name: \"" + hostname + "\""); 148 149 if (!oldHostname.equals(hostname)) { 150 var setHostnameRetCode = 151 shell.executeBashCommand( 152 "echo $NEW_HOSTNAME > /etc/hostname".replace("$NEW_HOSTNAME", hostname)); 153 setHostnameRetCode = shell.executeBashCommand("hostnamectl set-hostname " + hostname); 154 155 // Add to /etc/hosts 156 var addHostRetCode = 157 shell.executeBashCommand( 158 String.format( 159 "sed -i \"s/127.0.1.1.*%s/127.0.1.1\\t%s/g\" /etc/hosts", 160 oldHostname, hostname)); 161 162 shell.executeBashCommand("systemctl restart avahi-daemon.service"); 163 164 var success = setHostnameRetCode == 0 && addHostRetCode == 0; 165 if (!success) { 166 logger.error( 167 "Setting hostname returned non-zero codes (hostname/hosts) " 168 + setHostnameRetCode 169 + "|" 170 + addHostRetCode 171 + "!"); 172 } else { 173 logger.info("Set hostname to " + hostname); 174 } 175 } 176 } catch (Exception e) { 177 logger.error("Failed to set hostname!", e); 178 } 179 } 180 181 private void setConnectionDHCP(NetworkConfig config) { 182 String connName = "dhcp-" + config.networkManagerIface; 183 184 var shell = new ShellExec(); 185 try { 186 if (NetworkUtils.connDoesNotExist(connName)) { 187 logger.info("Creating DHCP connection " + connName); 188 shell.executeBashCommand( 189 NetworkingCommands.addConnectionCommand 190 .replace("${connection}", connName) 191 .replace("${interface}", config.networkManagerIface)); 192 } 193 logger.info("Updating the DHCP connection " + connName); 194 shell.executeBashCommand( 195 NetworkingCommands.modDHCPCommand.replace("${connection}", connName)); 196 // activate it 197 logger.info("Activating DHCP connection " + connName); 198 shell.executeBashCommand( 199 "nmcli connection up \"${connection}\"".replace("${connection}", connName), false); 200 activeConnections.put(config.networkManagerIface, connName); 201 } catch (Exception e) { 202 logger.error("Exception while setting DHCP!", e); 203 } 204 } 205 206 private void setConnectionStatic(NetworkConfig config) { 207 String connName = "static-" + config.networkManagerIface; 208 209 if (config.staticIp.isBlank()) { 210 logger.warn("Got empty static IP?"); 211 return; 212 } 213 214 // guess at the gateway from the staticIp 215 String[] parts = config.staticIp.split("\\."); 216 parts[parts.length - 1] = "1"; 217 String gateway = String.join(".", parts); 218 219 var shell = new ShellExec(); 220 try { 221 if (NetworkUtils.connDoesNotExist(connName)) { 222 // create connection 223 logger.info("Creating Static connection " + connName); 224 shell.executeBashCommand( 225 NetworkingCommands.addConnectionCommand 226 .replace("${connection}", connName) 227 .replace("${interface}", config.networkManagerIface)); 228 } 229 // modify it in case the static IP address is different 230 logger.info("Updating the Static connection " + connName); 231 shell.executeBashCommand( 232 NetworkingCommands.modStaticCommand 233 .replace("${connection}", connName) 234 .replace("${ipaddr}", config.staticIp) 235 .replace("${gateway}", gateway)); 236 // activate it 237 logger.info("Activating the Static connection " + connName); 238 shell.executeBashCommand( 239 "nmcli connection up \"${connection}\"".replace("${connection}", connName), false); 240 activeConnections.put(config.networkManagerIface, connName); 241 } catch (Exception e) { 242 logger.error("Error while setting static IP!", e); 243 } 244 } 245 246 // Detects changes in the carrier and reinitializes after re-connect 247 private void monitorDevice(String devName, int millisInterval) { 248 String taskName = "deviceStatus-" + devName; 249 if (TimedTaskManager.getInstance().taskActive(taskName)) { 250 // task is already running 251 return; 252 } 253 Path path = Paths.get("/sys/class/net/{device}/carrier".replace("{device}", devName)); 254 if (Files.notExists(path)) { 255 logger.error("Can't find " + path + ", so can't monitor " + devName); 256 return; 257 } 258 var last = 259 new Object() { 260 boolean carrier = true; 261 boolean exceptionLogged = false; 262 String addresses = ""; 263 }; 264 Runnable task = 265 () -> { 266 try { 267 boolean carrier = Files.readString(path).trim().equals("1"); 268 if (carrier != last.carrier) { 269 if (carrier) { 270 // carrier came back 271 logger.info("Interface " + devName + " has re-connected, reinitializing"); 272 reinitialize(); 273 } else { 274 logger.warn("Interface " + devName + " is disconnected, check Ethernet!"); 275 } 276 } 277 var iFace = NetworkInterface.getByName(devName); 278 if (iFace != null && iFace.isUp()) { 279 String tmpAddresses = ""; 280 tmpAddresses = iFace.getInterfaceAddresses().toString(); 281 if (!last.addresses.equals(tmpAddresses)) { 282 // addresses have changed, log the difference 283 last.addresses = tmpAddresses; 284 logger.info("Interface " + devName + " has address(es): " + last.addresses); 285 } 286 var conn = NetworkUtils.getActiveConnection(devName); 287 if (!conn.equals(activeConnections.get(devName))) { 288 logger.warn( 289 "Unexpected connection " 290 + conn 291 + " active on " 292 + devName 293 + ". Expected " 294 + activeConnections.get(devName)); 295 logger.info("Reinitializing"); 296 reinitialize(); 297 } 298 } 299 last.carrier = carrier; 300 last.exceptionLogged = false; 301 } catch (Exception e) { 302 if (!last.exceptionLogged) { 303 // Log the exception only once, but keep trying 304 logger.error("Could not check network status for " + devName, e); 305 last.exceptionLogged = true; 306 } 307 } 308 }; 309 310 TimedTaskManager.getInstance().addTask(taskName, task, millisInterval); 311 logger.debug("Watching network interface at path: " + path); 312 } 313}