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] mdns, montior writer에 프로세스 복구 로직 추가 #92

Merged
merged 13 commits into from
Dec 15, 2024
Merged
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
@@ -1,14 +1,17 @@
package com.whoz_in.log_writer.system_validator;
package com.whoz_in.log_writer.common;

import com.whoz_in.log_writer.common.NetworkInterface;
import com.whoz_in.log_writer.common.process.TransientProcess;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

//iwconfig의 출력을 파싱하여 네트워크 인터페이스들을 반환함
//prod 환경에선 iwconfig가 설치되어있을 것을 SystemValidator를 통해 보장한다.
@Profile("prod")
@Component
public class SystemNetworkInterfaces {
public final class IwconfigNetworkInterfaces implements SystemNetworkInterfaces {

//최신 정보를 가져온다.
public List<NetworkInterface> getLatest() {
List<NetworkInterface> interfaces = new ArrayList<>();
Expand All @@ -22,15 +25,17 @@ public List<NetworkInterface> getLatest() {
for (String line : iwconfigOutput) {
line = line.trim();
// 인터페이스 이름 감지 (인터페이스 정보 나오기 시작)
if (!line.startsWith(" ") && (line.contains("IEEE 802.11") || line.contains("unassociated"))) {
if (!line.startsWith(" ") && (line.contains("IEEE 802.11") || line.contains(
"unassociated"))) {
if (currentName != null) {
// 첫 인터페이스가 아니면 모아둔 이전 인터페이스의 정보 저장
interfaces.add(new NetworkInterface(currentName, currentEssid, currentMode));
}
// 새 인터페이스 정보 모으기 & 초기화
currentName = line.split("\\s+")[0];
if (line.contains("ESSID:"))
currentEssid = line.split("ESSID:")[1].split("\\s+")[0].replace("\"", "").trim();
currentEssid = line.split("ESSID:")[1].split("\\s+")[0].replace("\"", "")
.trim();
else
currentEssid = "";
currentMode = null; // 초기화
Expand All @@ -47,7 +52,7 @@ public List<NetworkInterface> getLatest() {
}
return interfaces;
}

}

/*
//샘플 iwconfig
Expand Down Expand Up @@ -84,4 +89,3 @@ public List<NetworkInterface> getLatest() {
" Tx excessive retries:0 Invalid misc:0 Missed beacon:0"
);
*/
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.whoz_in.log_writer.common;

import com.whoz_in.log_writer.config.NetworkConfig;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;


/*
기본적으로 맥에서 iwconfig를 못쓴다.
그래서 local에선 설정한 네트워크 인터페이스를 출력하는 이 가짜 객체를 만든 것이다.
*/
@Profile("local")
@Component
@RequiredArgsConstructor
public final class StubNetworkInterfaces implements SystemNetworkInterfaces{
private final NetworkConfig networkConfig;

@Override
public List<NetworkInterface> getLatest() {
return networkConfig.getNetworkInterfaces();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.whoz_in.log_writer.common;

import java.util.List;

//실시간으로 시스템에 존재하는 네트워크 인터페이스들을 얻을 수 있는 기능을 제공해야 한다.
public interface SystemNetworkInterfaces {
List<NetworkInterface> getLatest();
default boolean exists(NetworkInterface ni){
return getLatest().contains(ni);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

//실행 후 종료되지 않는 프로세스
//꾸준히 출력을 읽을 수 있어야 한다.
Expand Down Expand Up @@ -50,6 +52,19 @@ public String readLine(){
}
}

public List<String> readLines() {
List<String> lines = new ArrayList<>();
try {
String line;
while((line=this.br.readLine()) != null) {
lines.add(line);
}
return lines;
} catch (IOException e) {
throw new RuntimeException(e);
}
}

/**
* @return 프로세스의 에러 출력에서 한 줄을 읽어들인다.
* 읽을 줄이 없을경우 null을 출력한다.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.whoz_in.log_writer.common.process;

import jakarta.annotation.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
Expand All @@ -20,22 +19,31 @@ public class TransientProcess {

public TransientProcess() {}

// sudo 없이 실행할 커맨드
// command 예시: "ifconfig"
public TransientProcess(String command){
this(command, null);
}
public TransientProcess(String command, @Nullable String sudoPassword) {
try {
this.process = new ProcessBuilder(command.split(" "))
.redirectErrorStream(true)
.start();
this.br = new BufferedReader(new InputStreamReader(process.getInputStream()));
this.ebr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
if (sudoPassword==null) return;
Writer writer = new OutputStreamWriter(this.process.getOutputStream());
} catch (IOException e) {
throw new RuntimeException(command+" - 실행 실패");
}
this.br = new BufferedReader(new InputStreamReader(process.getInputStream()));
this.ebr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
}

// sudo로 실행할 커맨드
// sudoCommand 예시: "sudo iwconfig"
// sudo 없이 실행한 커맨드일 경우 writer에 flush하기도 전에 끝날 수 있으므로 나눠진 것
public TransientProcess(String sudoCommand, String sudoPassword) {
this(sudoCommand);
Writer writer = new OutputStreamWriter(this.process.getOutputStream());
try {
writer.write(sudoPassword + System.lineSeparator());
writer.flush();
} catch (IOException e) {
throw new RuntimeException("TransientProcess 실행 실패 -", e);
throw new RuntimeException(sudoCommand + " - sudo 명령어 입력 중 오류 발생");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,24 @@ public NetworkConfig(@Value("${spring.profiles.active:default}") String profile,
//monitor
Map<String, String> monitorMap = (Map<String, String>) map.get("monitor");
this.monitorInfo = new MonitorInfo(
generateCommand(monitorMap.get("command"), monitorMap.get("interface")));
this.networkInterfaces.stream()
.filter(ni->ni.getName().equals(monitorMap.get("interface")))
.findAny()
.orElseThrow(()->new IllegalStateException(monitorMap.get("interface")+"은 설정된 network_interfaces에 존재하지 않습니다.")),
generateCommand(monitorMap.get("command"), monitorMap.get("interface"))
);
// managed
Map<String, Object> managedMap = (Map<String, Object>) map.get("managed");
// mdns
Map<String, Object> mdnsMap = (Map<String, Object>) managedMap.get("mdns");
String mdnsCommand = (String) mdnsMap.get("command");
this.mdnsList = ((List<String>) mdnsMap.get("interfaces")).stream()
.map(interfaceName -> {
NetworkInterface mdnsNI = networkInterfaces.stream().filter(
ni -> ni.getName().equals(interfaceName)
).findAny().orElseThrow(()->new IllegalStateException(interfaceName+"은 설정된 network_interfaces에 존재하지 않습니다."));
return new ManagedInfo(interfaceName,
mdnsNI.getEssid(),
generateCommand(mdnsCommand, interfaceName));
NetworkInterface mdnsNI = networkInterfaces.stream()
.filter(ni -> ni.getName().equals(interfaceName))
.findAny()
.orElseThrow(()->new IllegalStateException(interfaceName+"은 설정된 network_interfaces에 존재하지 않습니다."));
return new ManagedInfo(mdnsNI, generateCommand(mdnsCommand, interfaceName));
})
.toList();
// arp
Expand All @@ -71,9 +75,7 @@ public NetworkConfig(@Value("${spring.profiles.active:default}") String profile,
NetworkInterface arpNI = networkInterfaces.stream().filter(
ni -> ni.getName().equals(interfaceName)
).findAny().orElseThrow(()->new IllegalStateException(interfaceName+"은 설정된 network_interfaces에 존재하지 않습니다."));
return new ManagedInfo(interfaceName,
arpNI.getEssid(),
generateCommand(arpCommand, interfaceName));
return new ManagedInfo(arpNI, generateCommand(arpCommand, interfaceName));
})
.toList();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.whoz_in.log_writer.managed;

import com.whoz_in.log_writer.common.NetworkInterface;

//Managed 프로세스를 생성하고 처리하는 과정에서 필요한 정보를 담는다.
public record ManagedInfo(String interfaceName, String ssid, String command) {}
public record ManagedInfo(NetworkInterface ni, String command) {}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import com.whoz_in.log_writer.common.process.TransientProcess;
import com.whoz_in.log_writer.managed.ManagedInfo;
import lombok.Getter;

@Getter
public class ArpLogProcess extends TransientProcess {
private final ManagedInfo info;
public ArpLogProcess(ManagedInfo info, String password) {
super(info.command(), password);
this.info = info;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
import com.whoz_in.log_writer.managed.ManagedInfo;
import com.whoz_in.log_writer.managed.ManagedLog;
import com.whoz_in.log_writer.managed.ManagedLogDAO;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

//TODO: 에러 로그 어떻게 관리할지 생각. 일단 TransientProcess라서 구현 안함

@Slf4j
@Component
public class ArpLogWriter {
Expand All @@ -34,36 +34,27 @@ public ArpLogWriter(ManagedLogDAO dao,
this.sudoPassword = sudoPassword;
}

//주기적으로 arp 명령어를 실행하여 로그를 저장함
@Scheduled(initialDelay = 10000, fixedDelay = 5000)
private void scan() {
List<ManagedLog> logs= arpList.stream()
.flatMap(arpInfo-> {
ArpLogProcess proc = new ArpLogProcess(arpInfo, sudoPassword); //프로세스 실행
List<String> lines = proc.resultList(); //프로세스의 모든 출력 가져오기
Set<ManagedLog> procLogs = lines.stream() //출력 라인들을 ManagedLog 변환하며 ssid도 넣어줌
.filter(parser::validate)
.map(line->{
ManagedLog log = parser.parse(line);
log.setSsid(arpInfo.ssid());
return log;
})
.collect(Collectors.toSet()); //Set으로 중복 제거

/*
Arp-scan은 단발성인데
Process의 isAlive()는 실행 중일 때도 false일 수 있고, 종료 중일 때도 true일 수 있으므로 오류의 판단이 힘들다.
따라서 Arp-scan의 경우 무조건 1개 이상의 결과가 나오므로 0개라면 실행 실패라고 판단한다.
*/
if (procLogs.isEmpty()) {
//SystemValidator가 시스템의 네트워크 인터페이스가 올바른지 검증하기 때문에 여기서는 warn으로 로깅
log.warn("[managed - arp({})] 실행 실패 : ERROR", arpInfo.ssid());
return Stream.empty();
}
log.info("[managed - arp({})] log to save : {}", arpInfo.ssid(), procLogs.size());
return procLogs.stream();
})
List<ManagedLog> logs = arpList.stream() //실행할 arp들을 스트림화
.map(info -> new ArpLogProcess(info, sudoPassword)) //arp 실행
.map(this::getLogsFromProcess) //arp 출력을 로그 Set으로 변환
.flatMap(Collection::stream) //Set끼리 합침
.toList();

dao.upsertAll(logs);
}

//프로세스의 출력들을 로그로 변환한다.
private Set<ManagedLog> getLogsFromProcess(ArpLogProcess process){
Set<ManagedLog> logs = process.resultList().stream()
.filter(parser::validate)
.map(parser::parse)
.collect(Collectors.toSet());//Set으로 중복 제거

String ssid = process.getInfo().ni().getEssid();
logs.forEach(log->log.setSsid(ssid));
log.info("[managed - arp({})] log to save : {}", ssid, logs.size());
return logs;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import com.whoz_in.log_writer.common.process.ContinuousProcess;
import com.whoz_in.log_writer.managed.ManagedInfo;
import lombok.Getter;

@Getter
public class MdnsLogProcess extends ContinuousProcess {

private final ManagedInfo info;
public MdnsLogProcess(ManagedInfo info, String sudoPassword) {
super(info.command(), sudoPassword);
this.info = info;
}
}
Loading
Loading