Files
creator-hub/cmd/control-plane/network.go
T

179 lines
5.6 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"git.ipao.vip/rogee/creator-hub/internal/hub"
)
const networkExitObservationURL = "https://ipinfo.io/json"
type networkExitProbe interface {
Check(context.Context, hub.NetworkExitAccess) (hub.ExitObservation, string)
}
type httpNetworkExitProbe struct {
endpoint string
client *http.Client
resolve func(hub.NetworkExitAccess) (string, error)
}
func defaultNetworkExitProbe() networkExitProbe {
return httpNetworkExitProbe{endpoint: networkExitObservationURL, client: &http.Client{Timeout: 20 * time.Second}, resolve: resolveExitCredential}
}
func (probe httpNetworkExitProbe) Check(ctx context.Context, exit hub.NetworkExitAccess) (hub.ExitObservation, string) {
proxyURL := &url.URL{Scheme: exit.Protocol, Host: net.JoinHostPort(exit.Host, fmt.Sprint(exit.Port))}
proxyUsername := ""
if exit.CredentialReference != nil {
secret, err := probe.resolve(exit)
if err != nil {
return hub.ExitObservation{}, "credential_unavailable"
}
username, password, found := strings.Cut(secret, ":")
if !found || username == "" {
return hub.ExitObservation{}, "credential_invalid"
}
proxyUsername = username
proxyURL.User = url.UserPassword(username, password)
}
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL)}
if exit.Protocol == "socks4" {
transport.Proxy = nil
transport.DialContext = socks4DialContext(proxyURL.Host, proxyUsername)
}
defer transport.CloseIdleConnections()
client := *probe.client
client.Transport = transport
request, err := http.NewRequestWithContext(ctx, http.MethodGet, probe.endpoint, nil)
if err != nil {
return hub.ExitObservation{}, "proxy_check_failed"
}
response, err := client.Do(request)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "auth") {
return hub.ExitObservation{}, "proxy_auth_failed"
}
return hub.ExitObservation{}, "proxy_check_failed"
}
defer response.Body.Close()
if response.StatusCode == http.StatusProxyAuthRequired {
return hub.ExitObservation{}, "proxy_auth_failed"
}
if response.StatusCode != http.StatusOK {
return hub.ExitObservation{}, "proxy_check_failed"
}
var observed struct {
IP string `json:"ip"`
Region string `json:"region"`
}
decoder := json.NewDecoder(io.LimitReader(response.Body, 64<<10))
if err := decoder.Decode(&observed); err != nil || net.ParseIP(observed.IP) == nil || len(observed.Region) > 64 {
return hub.ExitObservation{}, "exit_observation_invalid"
}
return hub.ExitObservation{PublicIP: observed.IP, Region: observed.Region}, ""
}
func socks4DialContext(proxyAddress, userID string) func(context.Context, string, string) (net.Conn, error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
connection, err := (&net.Dialer{}).DialContext(ctx, network, proxyAddress)
if err != nil {
return nil, err
}
failed := true
defer func() {
if failed {
_ = connection.Close()
}
}()
host, portText, err := net.SplitHostPort(address)
if err != nil {
return nil, errors.New("invalid SOCKS4 destination")
}
port, err := net.LookupPort("tcp", portText)
if err != nil {
return nil, errors.New("invalid SOCKS4 destination port")
}
request := []byte{4, 1, 0, 0, 0, 0, 0, 1}
binary.BigEndian.PutUint16(request[2:4], uint16(port))
if ip := net.ParseIP(host).To4(); ip != nil {
copy(request[4:8], ip)
}
request = append(request, userID...)
request = append(request, 0)
if net.ParseIP(host).To4() == nil {
request = append(request, host...)
request = append(request, 0)
}
if deadline, ok := ctx.Deadline(); ok {
_ = connection.SetDeadline(deadline)
}
if _, err := connection.Write(request); err != nil {
return nil, err
}
response := make([]byte, 8)
if _, err := io.ReadFull(connection, response); err != nil || response[1] != 90 {
return nil, errors.New("SOCKS4 proxy rejected connection")
}
_ = connection.SetDeadline(time.Time{})
failed = false
return connection, nil
}
}
// Secret managers and keyring bridges inject the referenced value at process start.
// Only the resolved username:password value is kept in the request-local call stack.
func resolveExitCredential(exit hub.NetworkExitAccess) (string, error) {
if exit.CredentialReference == nil || exit.CredentialKey == "" {
return "", errors.New("credential reference unavailable")
}
value, ok := os.LookupEnv(credentialEnvironmentName(exit.CredentialKey))
if !ok || value == "" {
return "", errors.New("credential value unavailable")
}
return value, nil
}
func credentialEnvironmentName(key string) string {
digest := sha256.Sum256([]byte(key))
return "CREATORHUB_CREDENTIAL_" + strings.ToUpper(hex.EncodeToString(digest[:]))
}
type gatewayNetworkExit struct {
Protocol string `json:"protocol"`
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
func gatewayNetworkExitFor(exit hub.NetworkExitAccess, resolve func(hub.NetworkExitAccess) (string, error)) (gatewayNetworkExit, error) {
result := gatewayNetworkExit{Protocol: exit.Protocol, Host: exit.Host, Port: exit.Port}
if exit.CredentialReference == nil {
return result, nil
}
secret, err := resolve(exit)
if err != nil {
return gatewayNetworkExit{}, errors.New("credential unavailable")
}
username, password, found := strings.Cut(secret, ":")
if !found || username == "" {
return gatewayNetworkExit{}, errors.New("credential invalid")
}
result.Username, result.Password = username, password
return result, nil
}