Path: blob/main/components/supervisor/pkg/ports/served-ports.go
2500 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package ports56import (7"bufio"8"bytes"9"context"10"encoding/hex"11"fmt"12"io"13"net"14"os"15"sort"16"strconv"17"strings"18"time"1920"github.com/gitpod-io/gitpod/common-go/log"21)2223// ServedPort describes a port served by a local service.24type ServedPort struct {25Address net.IP26Port uint3227BoundToLocalhost bool28}2930// ServedPortsObserver observes the locally served ports and provides31// full updates whenever that list changes.32type ServedPortsObserver interface {33// Observe starts observing the served ports until the context is canceled.34// The list of served ports is always the complete picture, i.e. if a single port changes,35// the whole list is returned.36// When the observer stops operating (because the context as canceled or an irrecoverable37// error occurred), the observer will close both channels.38Observe(ctx context.Context) (<-chan []ServedPort, <-chan error)39}4041const (42maxSubscriptions = 1004344fnNetTCP = "/proc/net/tcp"45fnNetTCP6 = "/proc/net/tcp6"46)4748// PollingServedPortsObserver regularly polls "/proc" to observe port changes.49type PollingServedPortsObserver struct {50RefreshInterval time.Duration5152fileOpener func(fn string) (io.ReadCloser, error)53}5455// Observe starts observing the served ports until the context is canceled.56func (p *PollingServedPortsObserver) Observe(ctx context.Context) (<-chan []ServedPort, <-chan error) {57if p.fileOpener == nil {58p.fileOpener = func(fn string) (io.ReadCloser, error) {59return os.Open(fn)60}61}6263var (64errchan = make(chan error, 1)65reschan = make(chan []ServedPort)66ticker = time.NewTicker(p.RefreshInterval)67)6869go func() {70defer close(errchan)71defer close(reschan)7273for {74select {75case <-ctx.Done():76log.Info("Port observer stopped")77return78case <-ticker.C:79}8081var (82visited = make(map[string]struct{})83ports []ServedPort84)8586var protos []string87for _, path := range []string{fnNetTCP, fnNetTCP6} {88if _, err := os.Stat(path); err == nil {89protos = append(protos, path)90}91}9293for _, fn := range protos {94fc, err := p.fileOpener(fn)95if err != nil {96errchan <- err97continue98}99ps, err := readNetTCPFile(fc, true)100fc.Close()101102if err != nil {103errchan <- err104continue105}106for _, port := range ps {107key := fmt.Sprintf("%s:%d", hex.EncodeToString(port.Address), port.Port)108_, exists := visited[key]109if exists {110continue111}112visited[key] = struct{}{}113ports = append(ports, port)114}115}116117if len(ports) > 0 {118reschan <- ports119}120}121}()122123return reschan, errchan124}125126func readNetTCPFile(fc io.Reader, listeningOnly bool) (ports []ServedPort, err error) {127scanner := bufio.NewScanner(fc)128for scanner.Scan() {129fields := strings.Fields(scanner.Text())130if len(fields) < 4 {131continue132}133if listeningOnly && fields[3] != "0A" {134continue135}136137segs := strings.Split(fields[1], ":")138if len(segs) < 2 {139continue140}141addrHex, portHex := segs[0], segs[1]142143port, err := strconv.ParseUint(portHex, 16, 32)144if err != nil {145log.WithError(err).WithField("port", portHex).Warn("cannot parse port entry from /proc/net/tcp* file")146continue147}148ipAddress := hexDecodeIP([]byte(addrHex))149150ports = append(ports, ServedPort{151BoundToLocalhost: ipAddress.IsLoopback(),152Address: ipAddress,153Port: uint32(port),154})155156sort.Slice(ports, func(i, j int) bool {157if ports[i].Address.Equal(ports[j].Address) {158return ports[i].Port < ports[j].Port159}160return bytes.Compare(ports[i].Address, ports[j].Address) < 0161})162163sort.Slice(ports, func(i, j int) bool {164return ports[i].Port < ports[j].Port165})166}167if err = scanner.Err(); err != nil {168return nil, err169}170171return172}173174// Parses IPv4/IPv6 addresses. The address is a big endian 32 bit ints, hex encoded.175// We just decode the hex and flip the bytes in every group of 4.176func hexDecodeIP(src []byte) net.IP {177buf := make(net.IP, net.IPv6len)178179blocks := len(src) / 8180for block := 0; block < blocks; block++ {181for i := 0; i < 4; i++ {182a := fromHexChar(src[block*8+i*2])183b := fromHexChar(src[block*8+i*2+1])184buf[block*4+3-i] = (a << 4) | b185}186}187return buf[:blocks*4]188}189190// Converts a hex character into its value.191func fromHexChar(c byte) uint8 {192switch {193case '0' <= c && c <= '9':194return c - '0'195case 'a' <= c && c <= 'f':196return c - 'a' + 10197case 'A' <= c && c <= 'F':198return c - 'A' + 10199}200return 0201}202203204