|
6 | 6 | "bytes" |
7 | 7 | "encoding/binary" |
8 | 8 | "fmt" |
| 9 | + "io" |
9 | 10 | "net" |
| 11 | + "net/http" |
10 | 12 | "os" |
11 | 13 | "os/exec" |
12 | 14 | "strconv" |
@@ -81,6 +83,15 @@ type NetworkInfo struct { |
81 | 83 | NetworkPrefix string `json:"networkPrefix,omitempty"` |
82 | 84 | BroadcastAddr string `json:"broadcastAddr,omitempty"` |
83 | 85 |
|
| 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 | + |
84 | 95 | Timestamp time.Time `json:"timestamp"` |
85 | 96 | } |
86 | 97 |
|
@@ -158,8 +169,16 @@ func GetNetworkInfo() (*NetworkInfo, error) { |
158 | 169 | info.HopsToInternet = countHopsToInternet() |
159 | 170 | }() |
160 | 171 |
|
| 172 | + go func() { |
| 173 | + defer wg.Done() |
| 174 | + info.PublicIp = getPublicIP() |
| 175 | + }() |
| 176 | + |
161 | 177 | wg.Wait() |
162 | 178 |
|
| 179 | + // NAT detection (needs public IP to be fetched first) |
| 180 | + detectNAT(info) |
| 181 | + |
163 | 182 | // VLAN detection |
164 | 183 | detectVLAN(iface.Name, info) |
165 | 184 |
|
@@ -868,3 +887,201 @@ func cidrToSubnet(ones int) string { |
868 | 887 | mask := net.CIDRMask(ones, 32) |
869 | 888 | return net.IP(mask).String() |
870 | 889 | } |
| 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 | +} |
0 commit comments