Go와 TUN/TAP: YAML 설정으로 구현하는 UDP 릴레이
Go 개발자는 TUN 인터페이스와 UDP 터널과 호환되는 네트워크 릴레이를 구축할 수 있습니다. 이 아키텍처는 입구-출구 체인을 정의하기 위한 YAML 구성 파일을 사용합니다. 각 릴레이는 양방향 트래픽—TUN과 UDP 사이, 그리고 다시 돌아오는 방식—을 처리하며, 논리적 하드코딩 없이도 복잡한 라우팅이 가능합니다.
핵심은 모든 노드에 io.ReadWriteCloser 인터페이스를 사용하는 것입니다. 로직은 간단합니다: 입구에서 패킷을 읽고 출구로 전달한 후, 반대 방향의 흐름도 처리합니다.
YAML 구성 구조
설정 파일은 입구/출구 쌍의 배열로 릴레이를 정의합니다. 파라미터에는 인터페이스 유형, IP 주소 할당, NAT 규칙, 인증 정보 등이 포함됩니다.
예시: TUN → TUN 연결
relays:
- ingress:
type: tun
name: tun10
cidr: "10.0.0.2/24"
peer: "10.0.0.1"
egress:
type: tun
name: tun11
cidr: "10.0.1.2/24"
peer: "10.0.1.1"
nat:
forward:
src: "10.0.1.1"
backward:
dst: "10.0.0.2"
UDP 릴레이를 만들려면 클라이언트 및 서버 엔드포인트를 추가하세요:
relays:
- ingress:
type: tun
name: tun10
cidr: "10.0.0.2/24"
peer: "10.0.0.1"
egress:
type: udp
dial: "localhost:4000"
password: "pass"
- ingress:
type: udp
listen: "localhost:4000"
password: "pass"
egress:
type: tun
name: tun11
cidr: "10.0.1.2/24"
peer: "10.0.1.1"
nat:
forward:
src: "10.0.1.1"
backward:
dst: "10.0.0.2"
이 구성은 다음과 같은 체인을 생성합니다: tun10 → UDP 클라이언트 → UDP 서버 → tun11. 트래픽은 모두 로컬에서 처리됩니다.
UDP 터널 프로토콜
각 IP 패킷에 헤더가 추가됩니다: 4바이트 타임스탬프 (uint32 BigEndian) + 16바이트 MD5 해시. 해시는 비밀번호 + 타임스탬프 + 패킷의 처음 64바이트를 기반으로 계산됩니다.
수신 시 시스템은 다음을 확인합니다:
- 패킷 크기 ≥ 헤더 크기 (20바이트)
- 타임스탬프가 현재 시간 기준 10초 이내
- 해시 일치 여부
패킷 해제 코드:
func (i *Ingress) Read(b []byte) (int, error) {
n, raddr, err := i.conn.ReadFrom(b)
if err != nil {
return 0, err
}
data, err := unpack(b[:n:n], i.pass)
if err != nil {
return 0, err
}
i.raddr = raddr
copy(b, data)
return len(data), nil
}
func unpack(packet []byte, pass string) ([]byte, error) {
if len(packet) < HeaderSize {
return nil, ErrSmallPacket
}
rtimestamp := binary.BigEndian.Uint32(packet[0:4])
rhash := [HashSize]byte(packet[4 : 4+HashSize])
payload := packet[HeaderSize:]
timestamp := uint32(time.Now().Unix())
if timestamp-rtimestamp > MaxTimeDiff && rtimestamp-timestamp > MaxTimeDiff {
return nil, ErrStalePacket
}
hash, err := calcHash(pass, payload, rtimestamp)
if err != nil {
return nil, fmt.Errorf("calc hash: %w", err)
}
if rhash != hash {
return nil, ErrWrongPass
}
return payload, nil
}
패킹은 대칭적입니다: calcHash + binary.BigEndian + 전송.
성능 테스트
tun10-tun11 간 iperf3를 사용한 로컬 테스트:
서버:
iperf3 -s -B 10.0.1.2
클라이언트:
iperf3 -c 10.0.1.2 -B 10.0.0.2
결과 (10초 기준):
| 구간 | 전송량 | 비트레이트 |
|----------|----------|---------|
| 0-1s | 113 MB | 941 Mbit/s |
| 1-2s | 109 MB | 921 Mbit/s |
| ... | ... | ... |
| 전체 | 1.08 GB | 925 Mbit/s (보내는 쪽) |
수신 쪽에서 약 922 Mbit/s. CPU: 릴레이 ~250% (M1), iperf3 ~10–30%. 배치 처리, 제로복사, SIMD 해싱 등을 통해 최적화 가능합니다.
분석을 위한 핵심 지표:
- 처리량: 평균 925 Mbit/s
- CPU 부하: 단일 스레드 고부하
- 지연 시간: 10초 타임스탬프 윈도우 (로컬 테스트에 적합함)
- 신뢰성: 해시 + 타임스탬프로 재전송 및 위조 공격 방지
핵심 요약
- YAML 구성 덕분에 재컴파일 없이 TUN/UDP 혼용 가능
- 프로토콜은 최소화됨: 20바이트 오버헤드, MD5 + 타임스탬프
- 로컬에서 성능 ~900+ Mbit/s, 하지만 CPU 한계 존재
io.ReadWriteCloser확장성: WireGuard, QUIC 등 쉽게 추가 가능- 구성 내 NAT 규칙으로 피어 투 피어 설정 간편화
— Editorial Team
아직 댓글이 없습니다.