1// Copyright (c) 2012-2014 Jeremy Latt 2// Copyright (c) 2016 Daniel Oaks <daniel@danieloaks.net> 3// released under the MIT license 4 5package utils 6 7import ( 8 "net" 9 "regexp" 10 "strings" 11) 12 13var ( 14 // subnet mask for an ipv6 /128: 15 mask128 = net.CIDRMask(128, 128) 16 IPv4LoopbackAddress = net.ParseIP("127.0.0.1").To16() 17 18 validHostnameLabelRegexp = regexp.MustCompile(`^[0-9A-Za-z.\-]+$`) 19) 20 21// AddrToIP returns the IP address for a net.Addr; unix domain sockets are treated as IPv4 loopback 22func AddrToIP(addr net.Addr) net.IP { 23 if tcpaddr, ok := addr.(*net.TCPAddr); ok { 24 return tcpaddr.IP.To16() 25 } else if _, ok := addr.(*net.UnixAddr); ok { 26 return IPv4LoopbackAddress 27 } else { 28 return nil 29 } 30} 31 32// IPStringToHostname converts a string representation of an IP address to an IRC-ready hostname 33func IPStringToHostname(ipStr string) string { 34 if 0 < len(ipStr) && ipStr[0] == ':' { 35 // fix for IPv6 hostnames (so they don't start with a colon), same as all other IRCds 36 ipStr = "0" + ipStr 37 } 38 return ipStr 39} 40 41// IsHostname returns whether we consider `name` a valid hostname. 42func IsHostname(name string) bool { 43 name = strings.TrimSuffix(name, ".") 44 if len(name) < 1 || len(name) > 253 { 45 return false 46 } 47 48 // ensure each part of hostname is valid 49 for _, part := range strings.Split(name, ".") { 50 if len(part) < 1 || len(part) > 63 || strings.HasPrefix(part, "-") || strings.HasSuffix(part, "-") { 51 return false 52 } 53 if !validHostnameLabelRegexp.MatchString(part) { 54 return false 55 } 56 } 57 58 return true 59} 60 61// IsServerName returns whether we consider `name` a valid IRC server name. 62func IsServerName(name string) bool { 63 // IRC server names specifically require a period 64 return IsHostname(name) && strings.IndexByte(name, '.') != -1 65} 66 67// Convenience to test whether `ip` is contained in any of `nets`. 68func IPInNets(ip net.IP, nets []net.IPNet) bool { 69 for _, network := range nets { 70 if network.Contains(ip) { 71 return true 72 } 73 } 74 return false 75} 76 77// NormalizeIPToNet represents an address (v4 or v6) as the v6 /128 CIDR 78// containing only it. 79func NormalizeIPToNet(addr net.IP) (network net.IPNet) { 80 // represent ipv4 addresses as ipv6 addresses, using the 4-in-6 prefix 81 // (actually this should be a no-op for any address returned by ParseIP) 82 addr = addr.To16() 83 // the network corresponding to this address is now an ipv6 /128: 84 return net.IPNet{ 85 IP: addr, 86 Mask: mask128, 87 } 88} 89 90// NormalizeNet normalizes an IPNet to a v6 CIDR, using the 4-in-6 prefix. 91// (this is like IP.To16(), but for IPNet instead of IP) 92func NormalizeNet(network net.IPNet) (result net.IPNet) { 93 if len(network.IP) == 16 { 94 return network 95 } 96 ones, _ := network.Mask.Size() 97 return net.IPNet{ 98 IP: network.IP.To16(), 99 // include the 96 bits of the 4-in-6 prefix 100 Mask: net.CIDRMask(96+ones, 128), 101 } 102} 103 104// Given a network, produce a human-readable string 105// (i.e., CIDR if it's actually a network, IPv6 address if it's a v6 /128, 106// dotted quad if it's a v4 /32). 107func NetToNormalizedString(network net.IPNet) string { 108 ones, bits := network.Mask.Size() 109 if ones == bits && ones == len(network.IP)*8 { 110 // either a /32 or a /128, output the address: 111 return network.IP.String() 112 } 113 return network.String() 114} 115 116// Parse a human-readable description (an address or CIDR, either v4 or v6) 117// into a normalized v6 net.IPNet. 118func NormalizedNetFromString(str string) (result net.IPNet, err error) { 119 _, network, err := net.ParseCIDR(str) 120 if err == nil { 121 return NormalizeNet(*network), nil 122 } 123 ip := net.ParseIP(str) 124 if ip == nil { 125 err = &net.AddrError{ 126 Err: "Couldn't interpret as either CIDR or address", 127 Addr: str, 128 } 129 return 130 } 131 return NormalizeIPToNet(ip), nil 132} 133 134// Parse a list of IPs and nets as they would appear in one of our config 135// files, e.g., proxy-allowed-from or a throttling exemption list. 136func ParseNetList(netList []string) (nets []net.IPNet, err error) { 137 var network net.IPNet 138 for _, netStr := range netList { 139 if netStr == "localhost" { 140 ipv4Loopback, _ := NormalizedNetFromString("127.0.0.0/8") 141 ipv6Loopback, _ := NormalizedNetFromString("::1/128") 142 nets = append(nets, ipv4Loopback) 143 nets = append(nets, ipv6Loopback) 144 continue 145 } 146 network, err = NormalizedNetFromString(netStr) 147 if err != nil { 148 return 149 } else { 150 nets = append(nets, network) 151 } 152 } 153 return 154} 155 156// Process the X-Forwarded-For header, validating against a list of trusted IPs. 157// Returns the address that the request was forwarded for, or nil if no trustworthy 158// data was available. 159func HandleXForwardedFor(remoteAddr string, xForwardedFor string, whitelist []net.IPNet) (result net.IP) { 160 // http.Request.RemoteAddr "has no defined format". with TCP it's typically "127.0.0.1:23784", 161 // with unix domain it's typically "@" 162 var remoteIP net.IP 163 host, _, err := net.SplitHostPort(remoteAddr) 164 if err != nil { 165 remoteIP = IPv4LoopbackAddress 166 } else { 167 remoteIP = net.ParseIP(host) 168 } 169 170 if remoteIP == nil || !IPInNets(remoteIP, whitelist) { 171 return remoteIP 172 } 173 174 // walk backwards through the X-Forwarded-For chain looking for an IP 175 // that is *not* trusted. that means it was added by one of our trusted 176 // forwarders (either remoteIP or something ahead of it in the chain) 177 // and we can trust it: 178 result = remoteIP 179 forwardedIPs := strings.Split(xForwardedFor, ",") 180 for i := len(forwardedIPs) - 1; i >= 0; i-- { 181 proxiedIP := net.ParseIP(strings.TrimSpace(forwardedIPs[i])) 182 if proxiedIP == nil { 183 return 184 } else if !IPInNets(proxiedIP, whitelist) { 185 return proxiedIP 186 } else { 187 result = proxiedIP 188 } 189 } 190 191 // no valid untrusted IPs were found in the chain; 192 // return either the last valid and trusted IP (which must be the origin), 193 // or nil: 194 return 195} 196 197// LookupHostname does an (optionally reverse-confirmed) hostname lookup 198// suitable for use as an IRC hostname. It falls back to a string 199// representation of the IP address (again suitable for use as an IRC 200// hostname). 201func LookupHostname(ip net.IP, forwardConfirm bool) (hostname string, lookupSuccessful bool) { 202 ipString := ip.String() 203 var candidate string 204 names, err := net.LookupAddr(ipString) 205 if err == nil && 0 < len(names) { 206 candidate = strings.TrimSuffix(names[0], ".") 207 } 208 if IsHostname(candidate) { 209 if forwardConfirm { 210 addrs, err := net.LookupHost(candidate) 211 if err == nil { 212 for _, addr := range addrs { 213 if forwardIP := net.ParseIP(addr); ip.Equal(forwardIP) { 214 hostname = candidate // successful forward confirmation 215 break 216 } 217 } 218 } 219 } else { 220 hostname = candidate 221 } 222 } 223 224 if hostname != "" { 225 return hostname, true 226 } else { 227 return IPStringToHostname(ipString), false 228 } 229} 230