mirror of
https://github.com/Alexander-D-Karpov/concord.git
synced 2026-03-16 22:04:15 +03:00
175 lines
4.2 KiB
Go
175 lines
4.2 KiB
Go
package netinfo
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Advertised struct {
|
|
PublicHost string // user-configured domain or detected public IP (if any)
|
|
LANHost string // local/LAN IP fallback
|
|
Port int
|
|
Source string // "config", "env", "http", "lan"
|
|
Notes []string
|
|
}
|
|
|
|
func ComputeAdvertised(ctx context.Context, userConfiguredHost, udpBindHost string, port int) Advertised {
|
|
adv := Advertised{Port: port}
|
|
|
|
if h := strings.TrimSpace(userConfiguredHost); h != "" {
|
|
h = trimScheme(h)
|
|
h = stripPort(h)
|
|
adv.PublicHost = h
|
|
adv.Source = "config"
|
|
} else if env := strings.TrimSpace(os.Getenv("CONCORD_PUBLIC_HOST")); env != "" {
|
|
h := trimScheme(env)
|
|
h = stripPort(h)
|
|
adv.PublicHost = h
|
|
adv.Source = "env"
|
|
} else {
|
|
if ip, err := detectPublicIP(ctx); err == nil && ip != "" {
|
|
adv.PublicHost = ip
|
|
adv.Source = "http"
|
|
}
|
|
}
|
|
|
|
if lan, err := detectLANIPPreferOutbound(); err == nil && lan != "" {
|
|
adv.LANHost = lan
|
|
} else if lan, err := firstPrivateIPv4(); err == nil && lan != "" {
|
|
adv.LANHost = lan
|
|
} else {
|
|
adv.LANHost = "127.0.0.1"
|
|
adv.Notes = append(adv.Notes, "Could not find a LAN IP; falling back to 127.0.0.1.")
|
|
}
|
|
|
|
if adv.PublicHost == "" {
|
|
adv.Source = "lan"
|
|
adv.Notes = append(adv.Notes,
|
|
"No public host detected. If this server is behind NAT, you may need port forwarding.",
|
|
`You can set a domain or public IP via config (e.g., Voice.PublicHost) or env CONCORD_PUBLIC_HOST.`,
|
|
)
|
|
}
|
|
if isAllInterfaces(udpBindHost) {
|
|
adv.Notes = append(adv.Notes, fmt.Sprintf("Server bound to %q; advertising detected addresses instead.", udpBindHost))
|
|
}
|
|
return adv
|
|
}
|
|
|
|
func trimScheme(h string) string {
|
|
h = strings.TrimSpace(h)
|
|
h = strings.TrimPrefix(h, "https://")
|
|
h = strings.TrimPrefix(h, "http://")
|
|
h = strings.TrimPrefix(h, "udp://")
|
|
h = strings.TrimPrefix(h, "tcp://")
|
|
return strings.TrimSuffix(h, "/")
|
|
}
|
|
|
|
func stripPort(hostWithPort string) string {
|
|
if idx := strings.LastIndex(hostWithPort, ":"); idx != -1 {
|
|
potentialPort := hostWithPort[idx+1:]
|
|
if _, err := strconv.Atoi(potentialPort); err == nil {
|
|
return hostWithPort[:idx]
|
|
}
|
|
}
|
|
return hostWithPort
|
|
}
|
|
|
|
func isAllInterfaces(h string) bool {
|
|
h = strings.TrimSpace(strings.ToLower(h))
|
|
return h == "" || h == "0.0.0.0" || h == "::" || h == "[::]" || h == "localhost"
|
|
}
|
|
|
|
func detectPublicIP(ctx context.Context) (string, error) {
|
|
client := &http.Client{Timeout: 2 * time.Second}
|
|
endpoints := []string{
|
|
"https://api.ipify.org?format=text",
|
|
"https://icanhazip.com",
|
|
}
|
|
|
|
for _, url := range endpoints {
|
|
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
b := make([]byte, 64)
|
|
n, _ := resp.Body.Read(b)
|
|
err = resp.Body.Close()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
ip := strings.TrimSpace(string(b[:n]))
|
|
if ip != "" && net.ParseIP(ip) != nil {
|
|
return ip, nil
|
|
}
|
|
}
|
|
return "", errors.New("no public IP endpoint reachable")
|
|
}
|
|
|
|
func detectLANIPPreferOutbound() (string, error) {
|
|
conn, err := net.Dial("udp", "1.1.1.1:80")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func(conn net.Conn) {
|
|
err := conn.Close()
|
|
if err != nil {
|
|
fmt.Println("error closing connection:", err)
|
|
}
|
|
}(conn)
|
|
localAddr := conn.LocalAddr()
|
|
udpAddr, ok := localAddr.(*net.UDPAddr)
|
|
if !ok || udpAddr.IP == nil {
|
|
return "", errors.New("no local UDP addr")
|
|
}
|
|
return udpAddr.IP.String(), nil
|
|
}
|
|
|
|
func firstPrivateIPv4() (string, error) {
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, iface := range ifaces {
|
|
// Skip down or loopback
|
|
if (iface.Flags&net.FlagUp) == 0 || (iface.Flags&net.FlagLoopback) != 0 {
|
|
continue
|
|
}
|
|
addrs, _ := iface.Addrs()
|
|
for _, a := range addrs {
|
|
ip, _, _ := net.ParseCIDR(a.String())
|
|
if ip == nil || ip.To4() == nil {
|
|
continue
|
|
}
|
|
if isPrivateIPv4(ip) {
|
|
return ip.String(), nil
|
|
}
|
|
}
|
|
}
|
|
return "", errors.New("no private IPv4 found")
|
|
}
|
|
|
|
func isPrivateIPv4(ip net.IP) bool {
|
|
ip4 := ip.To4()
|
|
if ip4 == nil {
|
|
return false
|
|
}
|
|
switch {
|
|
case ip4[0] == 10:
|
|
return true
|
|
case ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31:
|
|
return true
|
|
case ip4[0] == 192 && ip4[1] == 168:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|