Skip to content

Commit ad49c7b

Browse files
committed
feat: implement NAT detection and public IP retrieval in network info
1 parent c38c733 commit ad49c7b

2 files changed

Lines changed: 342 additions & 2 deletions

File tree

app/core/network_info.go

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"bytes"
77
"encoding/binary"
88
"fmt"
9+
"io"
910
"net"
11+
"net/http"
1012
"os"
1113
"os/exec"
1214
"strconv"
@@ -81,6 +83,15 @@ type NetworkInfo struct {
8183
NetworkPrefix string `json:"networkPrefix,omitempty"`
8284
BroadcastAddr string `json:"broadcastAddr,omitempty"`
8385

86+
// NAT Detection
87+
NatType string `json:"natType,omitempty"` // "None", "Single NAT", "Double NAT", "CGNAT", "Unknown"
88+
NatLayers int `json:"natLayers"` // Number of NAT layers detected
89+
BehindNat bool `json:"behindNat"` // Whether behind NAT
90+
BehindCgnat bool `json:"behindCgnat"` // Carrier-Grade NAT detected
91+
DoubleNat bool `json:"doubleNat"` // Multiple NAT layers
92+
NatGatewayIp string `json:"natGatewayIp,omitempty"` // First NAT gateway IP
93+
ExternalRouter string `json:"externalRouter,omitempty"` // External router if double NAT
94+
8495
Timestamp time.Time `json:"timestamp"`
8596
}
8697

@@ -158,8 +169,16 @@ func GetNetworkInfo() (*NetworkInfo, error) {
158169
info.HopsToInternet = countHopsToInternet()
159170
}()
160171

172+
go func() {
173+
defer wg.Done()
174+
info.PublicIp = getPublicIP()
175+
}()
176+
161177
wg.Wait()
162178

179+
// NAT detection (needs public IP to be fetched first)
180+
detectNAT(info)
181+
163182
// VLAN detection
164183
detectVLAN(iface.Name, info)
165184

@@ -868,3 +887,201 @@ func cidrToSubnet(ones int) string {
868887
mask := net.CIDRMask(ones, 32)
869888
return net.IP(mask).String()
870889
}
890+
891+
// getPublicIP fetches the public IP from external services
892+
func getPublicIP() string {
893+
// List of public IP services (try multiple for reliability)
894+
services := []string{
895+
"https://api.ipify.org",
896+
"https://ifconfig.me/ip",
897+
"https://icanhazip.com",
898+
"https://ipinfo.io/ip",
899+
"https://checkip.amazonaws.com",
900+
}
901+
902+
client := &http.Client{
903+
Timeout: 5 * time.Second,
904+
}
905+
906+
for _, service := range services {
907+
resp, err := client.Get(service)
908+
if err != nil {
909+
continue
910+
}
911+
defer resp.Body.Close()
912+
913+
if resp.StatusCode == http.StatusOK {
914+
body, err := io.ReadAll(resp.Body)
915+
if err != nil {
916+
continue
917+
}
918+
919+
ip := strings.TrimSpace(string(body))
920+
// Validate it's an IP
921+
if net.ParseIP(ip) != nil {
922+
return ip
923+
}
924+
}
925+
}
926+
927+
return ""
928+
}
929+
930+
// detectNAT analyzes NAT configuration and detects double NAT / CGNAT
931+
func detectNAT(info *NetworkInfo) {
932+
// No public IP means we couldn't determine NAT status
933+
if info.PublicIp == "" {
934+
info.NatType = "Unknown"
935+
return
936+
}
937+
938+
// Check if local IP equals public IP (no NAT)
939+
if info.Ipv4 == info.PublicIp {
940+
info.NatType = "None"
941+
info.BehindNat = false
942+
info.NatLayers = 0
943+
return
944+
}
945+
946+
// We're behind NAT
947+
info.BehindNat = true
948+
info.NatGatewayIp = info.Gateway
949+
950+
// Check for CGNAT (Carrier-Grade NAT)
951+
// CGNAT uses 100.64.0.0/10 range (RFC 6598)
952+
if isCGNATRange(info.PublicIp) || isCGNATRange(info.Gateway) {
953+
info.BehindCgnat = true
954+
info.NatType = "CGNAT"
955+
info.NatLayers = 2 // At minimum, CGNAT implies ISP NAT + your router
956+
return
957+
}
958+
959+
// Check if gateway is in private range
960+
gatewayPrivate := isPrivateIP(info.Gateway)
961+
962+
// Analyze traceroute for NAT layers
963+
natLayers, externalRouter := analyzeNATLayers(info.Gateway)
964+
info.NatLayers = natLayers
965+
info.ExternalRouter = externalRouter
966+
967+
// Determine NAT type
968+
if natLayers > 1 || (gatewayPrivate && externalRouter != "") {
969+
info.DoubleNat = true
970+
info.NatType = "Double NAT"
971+
} else if natLayers == 1 {
972+
info.NatType = "Single NAT"
973+
} else {
974+
info.NatType = "Single NAT"
975+
info.NatLayers = 1
976+
}
977+
}
978+
979+
// isCGNATRange checks if IP is in CGNAT range (100.64.0.0/10)
980+
func isCGNATRange(ipStr string) bool {
981+
ip := net.ParseIP(ipStr)
982+
if ip == nil {
983+
return false
984+
}
985+
986+
ip = ip.To4()
987+
if ip == nil {
988+
return false
989+
}
990+
991+
// CGNAT range: 100.64.0.0 - 100.127.255.255
992+
return ip[0] == 100 && ip[1] >= 64 && ip[1] <= 127
993+
}
994+
995+
// isPrivateIP checks if IP is in private ranges
996+
func isPrivateIP(ipStr string) bool {
997+
ip := net.ParseIP(ipStr)
998+
if ip == nil {
999+
return false
1000+
}
1001+
1002+
ip = ip.To4()
1003+
if ip == nil {
1004+
return false
1005+
}
1006+
1007+
// Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
1008+
if ip[0] == 10 {
1009+
return true
1010+
}
1011+
if ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31 {
1012+
return true
1013+
}
1014+
if ip[0] == 192 && ip[1] == 168 {
1015+
return true
1016+
}
1017+
1018+
return false
1019+
}
1020+
1021+
// analyzeNATLayers uses traceroute to detect multiple NAT layers
1022+
func analyzeNATLayers(gateway string) (int, string) {
1023+
// Run traceroute to a public IP
1024+
cmd := exec.Command("traceroute", "-n", "-m", "10", "-w", "1", "-q", "1", "8.8.8.8")
1025+
output, err := cmd.Output()
1026+
if err != nil {
1027+
// Try tracepath as fallback
1028+
cmd = exec.Command("tracepath", "-n", "-m", "10", "8.8.8.8")
1029+
output, err = cmd.Output()
1030+
if err != nil {
1031+
return 1, ""
1032+
}
1033+
}
1034+
1035+
lines := strings.Split(string(output), "\n")
1036+
var privateHops []string
1037+
var firstPublicHop string
1038+
1039+
for _, line := range lines {
1040+
line = strings.TrimSpace(line)
1041+
if line == "" {
1042+
continue
1043+
}
1044+
1045+
fields := strings.Fields(line)
1046+
if len(fields) < 2 {
1047+
continue
1048+
}
1049+
1050+
// Extract IP from traceroute output
1051+
var hopIP string
1052+
for _, field := range fields {
1053+
if net.ParseIP(field) != nil {
1054+
hopIP = field
1055+
break
1056+
}
1057+
}
1058+
1059+
if hopIP == "" || hopIP == "*" {
1060+
continue
1061+
}
1062+
1063+
// Skip if it's our gateway (first hop)
1064+
if hopIP == gateway {
1065+
continue
1066+
}
1067+
1068+
// Check if this hop is private
1069+
if isPrivateIP(hopIP) || isCGNATRange(hopIP) {
1070+
privateHops = append(privateHops, hopIP)
1071+
} else if firstPublicHop == "" {
1072+
firstPublicHop = hopIP
1073+
break // Stop at first public IP
1074+
}
1075+
}
1076+
1077+
// NAT layers = number of private hops before reaching public internet + 1
1078+
natLayers := len(privateHops) + 1
1079+
1080+
// External router is the last private hop before public internet
1081+
var externalRouter string
1082+
if len(privateHops) > 0 {
1083+
externalRouter = privateHops[len(privateHops)-1]
1084+
}
1085+
1086+
return natLayers, externalRouter
1087+
}

frontend/src/pages/Dashboard.jsx

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
Router,
1111
RefreshCw,
1212
MonitorSmartphone,
13-
Cpu
13+
Cpu,
14+
Shield,
15+
AlertTriangle,
16+
Layers,
17+
ExternalLink
1418
} from 'lucide-react'
1519
import StatsCard from '../components/dashboard/StatsCard'
1620
import InterfaceDetails from '../components/dashboard/InterfaceDetails'
@@ -233,6 +237,106 @@ export default function Dashboard({ networkData, connected }) {
233237
</StatsCard>
234238
</motion.div>
235239

240+
{/* NAT / Public IP Section */}
241+
<motion.div variants={itemVariants}>
242+
<div className={`glass-card p-6 border-l-4 ${
243+
data.natType === 'None' ? 'border-l-green-500' :
244+
data.natType === 'Single NAT' ? 'border-l-blue-500' :
245+
data.natType === 'Double NAT' ? 'border-l-yellow-500' :
246+
data.natType === 'CGNAT' ? 'border-l-red-500' :
247+
'border-l-dark-600'
248+
}`}>
249+
<div className="flex items-center gap-3 mb-6">
250+
<div className={`p-2 rounded-lg ${
251+
data.natType === 'None' ? 'bg-green-500/20' :
252+
data.natType === 'Single NAT' ? 'bg-blue-500/20' :
253+
data.natType === 'Double NAT' ? 'bg-yellow-500/20' :
254+
data.natType === 'CGNAT' ? 'bg-red-500/20' :
255+
'bg-dark-700/50'
256+
}`}>
257+
<Shield className={`w-5 h-5 ${
258+
data.natType === 'None' ? 'text-green-400' :
259+
data.natType === 'Single NAT' ? 'text-blue-400' :
260+
data.natType === 'Double NAT' ? 'text-yellow-400' :
261+
data.natType === 'CGNAT' ? 'text-red-400' :
262+
'text-dark-400'
263+
}`} />
264+
</div>
265+
<div className="flex-1">
266+
<h3 className="text-lg font-semibold text-white">NAT & Public IP</h3>
267+
<p className="text-sm text-dark-400">Network Address Translation status</p>
268+
</div>
269+
{/* NAT Status Badge */}
270+
<div className={`px-3 py-1.5 rounded-lg text-sm font-medium ${
271+
data.natType === 'None' ? 'bg-green-500/20 text-green-400' :
272+
data.natType === 'Single NAT' ? 'bg-blue-500/20 text-blue-400' :
273+
data.natType === 'Double NAT' ? 'bg-yellow-500/20 text-yellow-400' :
274+
data.natType === 'CGNAT' ? 'bg-red-500/20 text-red-400' :
275+
'bg-dark-700/50 text-dark-400'
276+
}`}>
277+
{data.natType || 'Detecting...'}
278+
</div>
279+
</div>
280+
281+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
282+
{/* Public IP */}
283+
<div className="space-y-1">
284+
<p className="text-xs text-dark-500 uppercase tracking-wide flex items-center gap-1">
285+
<ExternalLink className="w-3 h-3" />
286+
Public IP
287+
</p>
288+
<p className="text-lg font-mono text-white">{data.publicIp || 'Fetching...'}</p>
289+
</div>
290+
291+
{/* NAT Layers */}
292+
<div className="space-y-1">
293+
<p className="text-xs text-dark-500 uppercase tracking-wide flex items-center gap-1">
294+
<Layers className="w-3 h-3" />
295+
NAT Layers
296+
</p>
297+
<p className="text-lg font-semibold text-white">
298+
{data.natLayers !== undefined ? data.natLayers : '--'}
299+
{data.natLayers > 1 && <span className="text-yellow-400 text-sm ml-2">(Multiple)</span>}
300+
</p>
301+
</div>
302+
303+
{/* First NAT Gateway */}
304+
<div className="space-y-1">
305+
<p className="text-xs text-dark-500 uppercase tracking-wide">NAT Gateway</p>
306+
<p className="text-lg font-mono text-white">{data.natGatewayIp || data.gateway || '--'}</p>
307+
</div>
308+
309+
{/* External Router (if double NAT) */}
310+
<div className="space-y-1">
311+
<p className="text-xs text-dark-500 uppercase tracking-wide">External Router</p>
312+
<p className="text-lg font-mono text-white">{data.externalRouter || '--'}</p>
313+
</div>
314+
</div>
315+
316+
{/* NAT Warnings */}
317+
{(data.doubleNat || data.behindCgnat) && (
318+
<div className={`mt-4 p-3 rounded-lg flex items-start gap-3 ${
319+
data.behindCgnat ? 'bg-red-500/10 border border-red-500/20' :
320+
'bg-yellow-500/10 border border-yellow-500/20'
321+
}`}>
322+
<AlertTriangle className={`w-5 h-5 mt-0.5 flex-shrink-0 ${
323+
data.behindCgnat ? 'text-red-400' : 'text-yellow-400'
324+
}`} />
325+
<div>
326+
<p className={`font-medium ${data.behindCgnat ? 'text-red-400' : 'text-yellow-400'}`}>
327+
{data.behindCgnat ? 'Carrier-Grade NAT Detected' : 'Double NAT Detected'}
328+
</p>
329+
<p className="text-sm text-dark-400 mt-1">
330+
{data.behindCgnat
331+
? 'Your ISP is using CGNAT. This may prevent port forwarding and affect peer-to-peer connections. Contact your ISP for a public IP address.'
332+
: 'Multiple NAT layers detected. This can cause issues with port forwarding, VPNs, and gaming. Consider bridging one of your routers.'}
333+
</p>
334+
</div>
335+
</div>
336+
)}
337+
</div>
338+
</motion.div>
339+
236340
{/* Interface Details */}
237341
<motion.div variants={itemVariants}>
238342
<InterfaceDetails data={data} />
@@ -334,6 +438,25 @@ export default function Dashboard({ networkData, connected }) {
334438
<p className="text-xs text-dark-400 font-mono">{data.gateway || '--'}</p>
335439
</div>
336440

441+
{/* External Router (Double NAT) */}
442+
{data.externalRouter && (
443+
<>
444+
<div className="flex items-center">
445+
<div className="w-8 h-0.5 bg-dark-700"></div>
446+
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
447+
<div className="w-8 h-0.5 bg-dark-700"></div>
448+
</div>
449+
450+
<div className="flex flex-col items-center min-w-[100px]">
451+
<div className="w-16 h-16 rounded-xl bg-yellow-500/20 flex items-center justify-center mb-2">
452+
<Router className="w-8 h-8 text-yellow-400" />
453+
</div>
454+
<p className="text-sm font-medium text-white">ISP Router</p>
455+
<p className="text-xs text-dark-400 font-mono">{data.externalRouter}</p>
456+
</div>
457+
</>
458+
)}
459+
337460
{/* Connection Line */}
338461
<div className="flex items-center">
339462
<div className="w-8 h-0.5 bg-dark-700"></div>
@@ -347,7 +470,7 @@ export default function Dashboard({ networkData, connected }) {
347470
<Globe className="w-8 h-8 text-purple-400" />
348471
</div>
349472
<p className="text-sm font-medium text-white">Internet</p>
350-
<p className="text-xs text-dark-400">{data.latency ? `${data.latency.toFixed(0)}ms` : '--'}</p>
473+
<p className="text-xs text-dark-400 font-mono">{data.publicIp || (data.latency ? `${data.latency.toFixed(0)}ms` : '--')}</p>
351474
</div>
352475
</div>
353476
</div>

0 commit comments

Comments
 (0)