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.io.IOException;
021import java.net.NetworkInterface;
022import java.util.ArrayList;
023import java.util.List;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026import java.util.stream.Collectors;
027import org.photonvision.common.hardware.Platform;
028import org.photonvision.common.logging.LogGroup;
029import org.photonvision.common.logging.Logger;
030import org.photonvision.common.util.ShellExec;
031
032public class NetworkUtils {
033    private static final Logger logger = new Logger(NetworkUtils.class, LogGroup.General);
034
035    public enum NMType {
036        NMTYPE_ETHERNET("ethernet"),
037        NMTYPE_WIFI("wifi"),
038        NMTYPE_UNKNOWN("");
039
040        NMType(String id) {
041            identifier = id;
042        }
043
044        private final String identifier;
045
046        public static NMType typeForString(String s) {
047            for (var t : NMType.values()) {
048                if (t.identifier.equals(s)) {
049                    return t;
050                }
051            }
052            return NMTYPE_UNKNOWN;
053        }
054    }
055
056    public static class NMDeviceInfo {
057        public NMDeviceInfo(String c, String d, String type) {
058            connName = c;
059            devName = d;
060            nmType = NMType.typeForString(type);
061        }
062
063        public final String connName; // Human-readable name used by "nmcli con"
064        public final String devName; // underlying device, used by dhclient
065        public final NMType nmType;
066
067        @Override
068        public String toString() {
069            return "NMDeviceInfo [connName="
070                    + connName
071                    + ", devName="
072                    + devName
073                    + ", nmType="
074                    + nmType
075                    + "]";
076        }
077    }
078
079    private static List<NMDeviceInfo> allInterfaces = new ArrayList<>();
080    private static long lastReadTimestamp = 0;
081
082    public static List<NMDeviceInfo> getAllInterfaces() {
083        long now = System.currentTimeMillis();
084        if (now - lastReadTimestamp < 5000) return allInterfaces;
085        else lastReadTimestamp = now;
086
087        var ret = new ArrayList<NMDeviceInfo>();
088
089        if (!Platform.isLinux()) {
090            // Can only determine interface name on Linux, give up
091            return ret;
092        }
093
094        try {
095            var shell = new ShellExec(true, false);
096            shell.executeBashCommand(
097                    "nmcli -t -f GENERAL.CONNECTION,GENERAL.DEVICE,GENERAL.TYPE device show");
098            String out = shell.getOutput();
099            if (out == null) {
100                return new ArrayList<>();
101            }
102            Pattern pattern =
103                    Pattern.compile("GENERAL.CONNECTION:(.*)\nGENERAL.DEVICE:(.*)\nGENERAL.TYPE:(.*)");
104            Matcher matcher = pattern.matcher(out);
105            while (matcher.find()) {
106                if (!matcher.group(2).equals("lo")) {
107                    // only include non-loopback devices
108                    ret.add(new NMDeviceInfo(matcher.group(1), matcher.group(2), matcher.group(3)));
109                }
110            }
111        } catch (IOException e) {
112            logger.error("Could not get active network interfaces!", e);
113        }
114
115        logger.debug("Found network interfaces: " + ret);
116
117        allInterfaces = ret;
118        return ret;
119    }
120
121    public static List<NMDeviceInfo> getAllActiveInterfaces() {
122        // Seems like if an interface exists but isn't actually connected, the connection name will be
123        // an empty string. Check here and only return connections with non-empty names
124        return getAllInterfaces().stream()
125                .filter(it -> !it.connName.trim().isEmpty())
126                .collect(Collectors.toList());
127    }
128
129    public static List<NMDeviceInfo> getAllWiredInterfaces() {
130        return getAllInterfaces().stream()
131                .filter(it -> it.nmType.equals(NMType.NMTYPE_ETHERNET))
132                .collect(Collectors.toList());
133    }
134
135    public static List<NMDeviceInfo> getAllActiveWiredInterfaces() {
136        return getAllWiredInterfaces().stream()
137                .filter(it -> !it.connName.isBlank())
138                .collect(Collectors.toList());
139    }
140
141    public static NMDeviceInfo getNMinfoForConnName(String connName) {
142        for (NMDeviceInfo info : getAllActiveInterfaces()) {
143            if (info.connName.equals(connName)) {
144                return info;
145            }
146        }
147        return null;
148    }
149
150    public static NMDeviceInfo getNMinfoForDevName(String devName) {
151        for (NMDeviceInfo info : getAllActiveInterfaces()) {
152            if (info.devName.equals(devName)) {
153                return info;
154            }
155        }
156        logger.warn("Could not find a match for network device " + devName);
157        return null;
158    }
159
160    public static String getActiveConnection(String devName) {
161        var shell = new ShellExec(true, true);
162        try {
163            shell.executeBashCommand(
164                    "nmcli -g GENERAL.CONNECTION dev show \"" + devName + "\"", true, false);
165            return shell.getOutput().strip();
166        } catch (Exception e) {
167            logger.error("Exception from nmcli!");
168        }
169        return "";
170    }
171
172    public static boolean connDoesNotExist(String connName) {
173        var shell = new ShellExec(true, true);
174        try {
175            shell.executeBashCommand(
176                    "nmcli -g GENERAL.STATE connection show \"" + connName + "\"", true, false);
177            return (shell.getExitCode() == 10);
178        } catch (Exception e) {
179            logger.error("Exception from nmcli!");
180        }
181        return false;
182    }
183
184    public static String getIPAddresses(String iFaceName) {
185        if (iFaceName == null || iFaceName.isBlank()) {
186            return "";
187        }
188        List<String> addresses = new ArrayList<String>();
189        try {
190            var iFace = NetworkInterface.getByName(iFaceName);
191            if (iFace != null && iFace.isUp()) {
192                for (var addr : iFace.getInterfaceAddresses()) {
193                    var addrStr = addr.getAddress().toString();
194                    if (addrStr.startsWith("/")) {
195                        addrStr = addrStr.substring(1);
196                    }
197                    addrStr = addrStr + "/" + addr.getNetworkPrefixLength();
198                    addresses.add(addrStr);
199                }
200            }
201        } catch (Exception e) {
202            e.printStackTrace();
203        }
204        return String.join(", ", addresses);
205    }
206}