package encdns import ( "bufio" "encoding/hex" "fmt" "net/netip" "os" "strconv" "strings" "jarl/internal/model" ) // Socket is a listening TCP socket on this host. type Socket struct { Addr netip.Addr Port int } // ListeningSockets reads the host's listening TCP sockets from // /proc/net/tcp and tcp6 (Linux; nil elsewhere). It needs no privileges // and binds nothing. Tests replace it. var ListeningSockets = func() ([]Socket, error) { var out []Socket for _, f := range []string{"/proc/net/tcp", "/proc/net/tcp6"} { socks, err := parseProcNet(f) if err != nil { if os.IsNotExist(err) { continue } return nil, err } out = append(out, socks...) } return out, nil } func parseProcNet(path string) ([]Socket, error) { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var out []Socket sc := bufio.NewScanner(f) sc.Scan() // header for sc.Scan() { fields := strings.Fields(sc.Text()) if len(fields) < 4 || fields[3] != "0A" { // 0A: LISTEN continue } s, err := parseProcAddr(fields[1]) if err == nil { out = append(out, s) } } return out, sc.Err() } // parseProcAddr parses "0100007F:0035": the address in 32-bit words of // host byte order (little-endian here), then the port. func parseProcAddr(s string) (Socket, error) { ha, hp, ok := strings.Cut(s, ":") if !ok { return Socket{}, fmt.Errorf("bad address %q", s) } b, err := hex.DecodeString(ha) if err != nil || (len(b) != 4 && len(b) != 16) { return Socket{}, fmt.Errorf("bad address %q", s) } for i := 0; i+4 <= len(b); i += 4 { b[i], b[i+1], b[i+2], b[i+3] = b[i+3], b[i+2], b[i+1], b[i] } addr, _ := netip.AddrFromSlice(b) port, err := strconv.ParseUint(hp, 16, 16) if err != nil { return Socket{}, err } return Socket{Addr: addr.Unmap(), Port: int(port)}, nil } // Conflict is something else listening where a listener would go. type Conflict struct { Address string `json:"address"` Port int `json:"port"` What string `json:"what"` // who it is, when Jarl knows } // PortCheck says which of addresses × ports something else listens on // already. named's own listeners (the setup's, now) and jarld's (known // listen addresses) are told apart. func PortCheck(st *model.State, addresses []string, ports []int, jarld []string) ([]Conflict, error) { socks, err := ListeningSockets() if err != nil { return nil, err } // What named listens on now with DoT or DoH (it will again). ours := map[string]bool{} for _, l := range Listeners(st) { if l.Kind() != "plain" { for _, a := range l.Addrs { ours[a+"#"+strconv.Itoa(l.EffectivePort())] = true } } } jarldOn := map[string]bool{} for _, l := range jarld { jarldOn[l] = true } var out []Conflict for _, as := range addresses { a, err := netip.ParseAddr(as) if err != nil { continue } a = a.Unmap() for _, p := range ports { if ours[a.String()+"#"+strconv.Itoa(p)] { continue } for _, s := range socks { if s.Port != p || !(s.Addr == a || s.Addr.IsUnspecified() && s.Addr.Is4() == a.Is4() || s.Addr.IsUnspecified() && s.Addr.Is6()) { continue } what := "another program" for l := range jarldOn { if strings.HasSuffix(l, ":"+strconv.Itoa(p)) { what = "jarld (Jarl's web UI and API)" } } out = append(out, Conflict{Address: a.String(), Port: p, What: what}) break } } } return out, nil } // FreeDoHPort returns the first of the usual DoH ports nothing listens on // at the addresses (0 when none is free). func FreeDoHPort(st *model.State, addresses []string, jarld []string) int { for _, p := range []int{443, 8443, 4443, 9443} { if c, err := PortCheck(st, addresses, []int{p}, jarld); err == nil && len(c) == 0 { return p } } return 0 }