Browse Source

finish implementing LocalSOCKS5Address and fix a couple bugs

multi-tenant
forest 2 months ago
parent
commit
7816c9d137
7 changed files with 219 additions and 80 deletions
  1. +1
    -0
      README.md
  2. +2
    -2
      build.sh
  3. +1
    -0
      go.mod
  4. +2
    -0
      go.sum
  5. +210
    -76
      main_client.go
  6. +1
    -1
      main_server.go
  7. +2
    -1
      tunnel-lib/virtualaddr.go

+ 1
- 0
README.md View File

@ -28,6 +28,7 @@ To edit it, download the <a download href="readme/diagram.drawio">diagram file</
1. The tunnel client connects to the tunnel server on the Tunnel Control Port. This connection can use TLS Client Authentication. This connection will be held open and re-created if dropped.
1. An internet user connects to the tunnel server on one of the ports defined in the JSON. The internet user's request is tunneled through the original connection from the tunnel client, and then proxied to the web server software running on the self-hoster's server computer.
Note: if you wish to easily create server/client key pairs that will work with threshold, see: https://git.sequentialread.com/forest/make-fake-cert
### Output from Usage example showing how it works:


+ 2
- 2
build.sh View File

@ -4,7 +4,7 @@ function build() {
rm -rf build
mkdir build
GOOS=linux GOARCH=$1 go build -o build/threshold
GOOS=linux GOARCH=$1 go build -tags 'osusergo netgo' -ldflags='-extldflags=-static' -o build/threshold
sha256sum build/threshold
@ -44,6 +44,6 @@ function build() {
}
#build arm
build arm
build amd64
#build arm64

+ 1
- 0
go.mod View File

@ -3,6 +3,7 @@ module git.sequentialread.com/forest/threshold
go 1.14
require (
git.sequentialread.com/forest/pkg-errors v0.9.2 // indirect
github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a
github.com/cenkalti/backoff v2.1.0+incompatible
github.com/gorilla/websocket v1.4.0


+ 2
- 0
go.sum View File

@ -1,3 +1,5 @@
git.sequentialread.com/forest/pkg-errors v0.9.2 h1:j6pwbL6E+TmE7TD0tqRtGwuoCbCfO6ZR26Nv5nest9g=
git.sequentialread.com/forest/pkg-errors v0.9.2/go.mod h1:8TkJ/f8xLWFIAid20aoqgDZcCj9QQt+FU+rk415XO1w=
git.sequentialread.com/forest/threshold v0.0.0-20170601195443-35a8b95662bf h1:2flo/nnhfe3sSxQ/MHlK7KoY54tQ1pAvMzkh0ZOxyH4=
git.sequentialread.com/forest/threshold v0.0.0-20170601195443-35a8b95662bf/go.mod h1:i+PvDDsWjggoCQOO8bGJJKRB9qfxmHk5yzIEA/h8dzg=
github.com/armon/go-proxyproto v0.0.0-20180202201750-5b7edb60ff5f h1:SaJ6yqg936TshyeFZqQE+N+9hYkIeL9AMr7S4voCl10=


+ 210
- 76
main_client.go View File

@ -1,12 +1,14 @@
package main
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
@ -19,9 +21,12 @@ import (
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
errors "git.sequentialread.com/forest/pkg-errors"
tunnel "git.sequentialread.com/forest/threshold/tunnel-lib"
"git.sequentialread.com/forest/threshold/tunnel-lib/proto"
proxyprotocol "github.com/armon/go-proxyproto"
@ -29,21 +34,26 @@ import (
)
type ClientConfig struct {
DebugLog bool
ClientId string
GreenhouseDomain string
GreenhouseAPIToken string
GreenhouseThresholdPort int
ServerAddr string
Servers []string
ServiceToLocalAddrMap *map[string]string
CaCertificateFilesGlob string
ClientTlsKeyFile string
ClientTlsCertificateFile string
CaCertificate string
ClientTlsKey string
ClientTlsCertificate string
LocalSOCKS5Address string // use this when a local proxy is required to talk to the threshold server.
DebugLog bool
ClientId string
GreenhouseDomain string
GreenhouseAPIToken string
GreenhouseThresholdPort int
ServerAddr string
Servers []string
DefaultTunnels *LiveConfigUpdate
CaCertificateFilesGlob string
ClientTlsKeyFile string
ClientTlsCertificateFile string
CaCertificate string
ClientTlsKey string
ClientTlsCertificate string
// use this when a local proxy is required to talk to the threshold server.
// if you set the hostname to "gateway", like "LocalSOCKS5Address": "gateway:1080"
// then it will try to SOCKS5 connect to any/all default gateways (routers) on the given port (1080 in this case).
LocalSOCKS5Address string
AdminUnixSocket string
AdminAPIPort int
AdminAPICACertificateFile string
@ -168,8 +178,8 @@ func runClient(configFileName *string) {
clientServers = []ClientServer{makeServer(config.ServerAddr)}
}
if config.ServiceToLocalAddrMap != nil {
serviceToLocalAddrMap = config.ServiceToLocalAddrMap
if config.DefaultTunnels != nil {
serviceToLocalAddrMap = &config.DefaultTunnels.ServiceToLocalAddrMap
} else {
serviceToLocalAddrMap = &(map[string]string{})
}
@ -183,17 +193,39 @@ func runClient(configFileName *string) {
configToLogString,
"$1******$2",
)
configToLogString = regexp.MustCompile(
`("(CaCertificate|ClientTlsKey|ClientTlsCertificate)": "[^"]{27})[^"]+([^"]{27}")`,
).ReplaceAllString(
configToLogString,
"$1 blahblahPEMblahblah $3",
)
log.Printf("theshold client is starting up using config:\n%s\n", configToLogString)
var proxyDialer proxy.Dialer = nil
dialFunction := net.Dial
if config.LocalSOCKS5Address != "" {
dialer, err := proxy.SOCKS5("tcp", "PROXY_IP", nil, proxy.Direct)
proxyDialer, err = getProxyDialer(config.LocalSOCKS5Address)
if err != nil {
log.Fatal("can't connect to the proxy:", err)
log.Fatalf("can't start because can't getProxyDialer(): %+v", err)
}
dialFunction = func(network, address string) (net.Conn, error) {
var err error
if proxyDialer == nil {
proxyDialer, err = getProxyDialer(config.LocalSOCKS5Address)
if err != nil {
return nil, errors.Wrap(err, "dialFunction failed to recreate proxyDialer: ")
}
}
// if it fails, set it to null so it will be re-created // TODO test this and verify it actually works 0__0
conn, err := proxyDialer.Dial(network, address)
if err != nil {
proxyDialer = nil
}
return conn, err
}
dialFunction = dialer.Dial
}
var cert tls.Certificate
@ -202,12 +234,12 @@ func runClient(configFileName *string) {
if hasFiles && !hasLiterals {
cert, err = tls.LoadX509KeyPair(config.ClientTlsCertificateFile, config.ClientTlsKeyFile)
if err != nil {
log.Fatal(fmt.Sprintf("can't start because tls.LoadX509KeyPair returned: \n%+v\n", err))
log.Fatalf("can't start because tls.LoadX509KeyPair returned: \n%+v\n", err)
}
} else if !hasFiles && hasLiterals {
cert, err = tls.X509KeyPair([]byte(config.ClientTlsCertificate), []byte(config.ClientTlsKey))
if err != nil {
log.Fatal(fmt.Sprintf("can't start because tls.X509KeyPair returned: \n%+v\n", err))
log.Fatalf("can't start because tls.X509KeyPair returned: \n%+v\n", err)
}
} else {
@ -286,7 +318,13 @@ func runClient(configFileName *string) {
if err != nil {
return nil, err
}
tlsConn := tls.Client(conn, tlsClientConfig)
addressSplit := strings.Split(address, ":")
tlsConn := tls.Client(conn, &tls.Config{
ServerName: addressSplit[0],
Certificates: tlsClientConfig.Certificates,
RootCAs: tlsClientConfig.RootCAs,
})
err = tlsConn.Handshake()
if err != nil {
return nil, err
@ -339,6 +377,20 @@ func runClient(configFileName *string) {
for {
stateChange := <-clientStateChanges
log.Printf("%s clientStateChange: %s\n", server.ServerHostPort, stateChange.String())
if config.DefaultTunnels != nil && stateChange.Current == tunnel.ClientConnected {
go (func() {
failed := true
for failed {
err := updateListenersOnServer(config.DefaultTunnels.Listeners)
if err != nil {
log.Printf("DefaultTunnels: failed to updateListenersOnServer(): %+v\nRetrying in 5 seconds...\n", err)
time.Sleep(time.Second * 5)
} else {
failed = false
}
}
})()
}
}
})()
@ -463,67 +515,17 @@ func (handler clientAdminAPI) ServeHTTP(response http.ResponseWriter, request *h
return
}
sendBytes, err := json.Marshal(configUpdate.Listeners)
err = updateListenersOnServer(configUpdate.Listeners)
if err != nil {
log.Printf("clientAdminAPI: Listeners json serialization failed: %+v\n\n", err)
http.Error(response, "500 Listeners json serialization failed", http.StatusInternalServerError)
log.Printf("clientAdminAPI: can't updateListenersOnServer(): %+v\n\n", err)
http.Error(response, "500 internal server error", http.StatusInternalServerError)
return
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsClientConfig,
},
Timeout: 10 * time.Second,
}
// TODO make this concurrent requests, not one by one.
for _, server := range clientServers {
apiURL := fmt.Sprintf("https://%s/tunnels", server.ServerHostPort)
tunnelsRequest, err := http.NewRequest("PUT", apiURL, bytes.NewReader(sendBytes))
if err != nil {
log.Printf("clientAdminAPI: error creating tunnels request: %+v\n\n", err)
http.Error(response, "500 error creating tunnels request", http.StatusInternalServerError)
return
}
tunnelsRequest.Header.Add("content-type", "application/json")
tunnelsResponse, err := client.Do(tunnelsRequest)
if err != nil {
log.Printf("clientAdminAPI: Do(tunnelsRequest): %+v\n\n", err)
http.Error(response, "502 tunnels request failed", http.StatusBadGateway)
return
}
tunnelsResponseBytes, err := ioutil.ReadAll(tunnelsResponse.Body)
if err != nil {
log.Printf("clientAdminAPI: tunnelsResponse read error: %+v\n\n", err)
http.Error(response, "502 tunnelsResponse read error", http.StatusBadGateway)
return
}
if tunnelsResponse.StatusCode != http.StatusOK {
log.Printf(
"clientAdminAPI: tunnelsRequest returned HTTP %d: %s\n\n",
tunnelsResponse.StatusCode, string(tunnelsResponseBytes),
)
http.Error(
response,
fmt.Sprintf("502 tunnels request returned HTTP %d: %s", tunnelsResponse.StatusCode, string(tunnelsResponseBytes)),
http.StatusBadGateway,
)
return
}
}
if &configUpdate.ServiceToLocalAddrMap != nil {
serviceToLocalAddrMap = &configUpdate.ServiceToLocalAddrMap
}
// cache the listeners locally for use in test mode.
testModeListeners = map[string]ListenerConfig{}
for _, listener := range configUpdate.Listeners {
testModeListeners[listener.BackEndService] = listener
}
response.Header().Add("content-type", "application/json")
response.WriteHeader(http.StatusOK)
response.Write(requestBytes)
@ -538,6 +540,50 @@ func (handler clientAdminAPI) ServeHTTP(response http.ResponseWriter, request *h
}
func updateListenersOnServer(listeners []ListenerConfig) error {
sendBytes, err := json.Marshal(listeners)
if err != nil {
return errors.Wrap(err, "Listeners json serialization failed")
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: tlsClientConfig,
},
Timeout: 10 * time.Second,
}
// TODO make this concurrent requests, not one by one.
for _, server := range clientServers {
apiURL := fmt.Sprintf("https://%s/tunnels", server.ServerHostPort)
tunnelsRequest, err := http.NewRequest("PUT", apiURL, bytes.NewReader(sendBytes))
if err != nil {
return errors.Wrap(err, "error creating tunnels request")
}
tunnelsRequest.Header.Add("content-type", "application/json")
tunnelsResponse, err := client.Do(tunnelsRequest)
if err != nil {
return errors.Wrap(err, "tunnels request failed")
}
tunnelsResponseBytes, err := ioutil.ReadAll(tunnelsResponse.Body)
if err != nil {
return errors.Wrap(err, "tunnels request response read error")
}
if tunnelsResponse.StatusCode != http.StatusOK {
return errors.Errorf("tunnelsRequest returned HTTP %d: %s", tunnelsResponse.StatusCode, string(tunnelsResponseBytes))
}
}
// cache the listeners locally for use in test mode.
testModeListeners = map[string]ListenerConfig{}
for _, listener := range listeners {
testModeListeners[listener.BackEndService] = listener
}
return nil
}
func handleTestConnection(remote net.Conn, msg *proto.ControlMessage) {
listenerInfo, hasListenerInfo := testModeListeners[msg.Service]
log.Printf("handleTestConnection: %s (%s, %d)", msg.Service, listenerInfo.ListenHostnameGlob, listenerInfo.ListenPort)
@ -644,3 +690,91 @@ func GenerateTestX509Cert() (tls.Certificate, error) {
return outCert, nil
}
func getProxyDialer(socks5Address string) (proxy.Dialer, error) {
if strings.HasPrefix(strings.ToLower(socks5Address), "gateway") {
splitAddress := strings.Split(socks5Address, ":")
if len(splitAddress) != 2 {
return nil, errors.Errorf("can't getProxyDialer() because LocalSOCKS5Address '%s' was invalid. should be of the form host:port")
}
port := splitAddress[1]
defaultGateways, err := getDefaultGatewaysFromRoutingTable()
if err != nil {
return nil, errors.Errorf("can't getProxyDialer() because LocalSOCKS5Address was set to '%s' but: \n%+v\n", socks5Address, err)
}
if len(defaultGateways) == 0 {
return nil, errors.Errorf(
"can't getProxyDialer() because LocalSOCKS5Address was set to '%s' but no default gateways were found in routing table",
socks5Address,
)
}
failures := make([]string, len(defaultGateways))
for i := 0; i < len(defaultGateways); i++ {
address := fmt.Sprintf("%s:%s", defaultGateways[i], port)
conn, err := net.Dial("tcp", address)
if err == nil {
conn.Close()
return proxy.SOCKS5("tcp", address, nil, proxy.Direct)
}
failures = append(failures, fmt.Sprintf("can't connect to %s", address))
}
// if we got this far it means we tried them all and none of them worked.
return nil, errors.Errorf("can't connect to LocalSOCKS5Address '%s': %s", socks5Address, strings.Join(failures, ", "))
} else {
conn, err := net.Dial("tcp", socks5Address)
if err != nil {
return nil, errors.Errorf("can't connect to LocalSOCKS5Address '%s': %s", socks5Address, err)
}
conn.Close()
return proxy.SOCKS5("tcp", socks5Address, nil, proxy.Direct)
}
}
// https://stackoverflow.com/questions/40682760/what-syscall-method-could-i-use-to-get-the-default-network-gateway
func getDefaultGatewaysFromRoutingTable() ([]string, error) {
if runtime.GOOS != "linux" {
return nil, errors.Errorf("getDefaultGatewaysFromRoutingTable() does not support %s operating system yet.", runtime.GOOS)
}
toReturn := []string{}
file, err := os.Open("/proc/net/route")
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
if scanner.Scan() { // skip the first line (header)
for scanner.Scan() {
tokens := strings.Split(scanner.Text(), "\t")
destinationHex := "0x" + tokens[1]
gatewayHex := "0x" + tokens[2]
destinationInt, err := strconv.ParseInt(destinationHex, 0, 64)
if err != nil {
return nil, err
}
gatewayInt, err := strconv.ParseInt(gatewayHex, 0, 64)
if err != nil {
return nil, err
}
// 0 means 0.0.0.0 -- we are looking for default routes, routes that have universal destination 0.0.0.0
if destinationInt == 0 && gatewayInt != 0 {
gatewayUint32 := uint32(gatewayInt)
// make net.IP address from uint32
ip := make(net.IP, 4)
binary.LittleEndian.PutUint32(ip, gatewayUint32)
toReturn = append(toReturn, ip.String())
//fmt.Printf("%T --> %[1]v\n", ipBytes)
}
}
}
return toReturn, nil
}

+ 1
- 1
main_server.go View File

@ -361,7 +361,7 @@ func setListeners(tenantId string, listenerConfigs []ListenerConfig) (int, strin
if err != nil {
if strings.Contains(err.Error(), "already in use") {
return http.StatusConflict, fmt.Sprintf("Port Conflict: Port %s is reserved or already in use", listenAddress)
return http.StatusConflict, fmt.Sprintf("Port Conflict: Port %s:%d is reserved or already in use", listenAddress, newListenerConfig.ListenPort)
}
log.Printf("setListeners(): can't net.Listen(\"tcp\", \"%s\") because %s \n", listenAddress, err)


+ 2
- 1
tunnel-lib/virtualaddr.go View File

@ -208,13 +208,14 @@ func (vaddr *vaddrStorage) Delete(ip net.IP, port int, hostnameGlob string) {
func (vaddr *vaddrStorage) newListener(ip net.IP, port int) (*listener, error) {
listenAddress := net.JoinHostPort(ip.String(), strconv.Itoa(port))
fmt.Printf("now listening on %s\n\n", listenAddress)
netListener, err := net.Listen("tcp", listenAddress)
if err != nil {
return nil, err
}
fmt.Printf("now listening on %s\n\n", listenAddress)
return &listener{
Listener: netListener,
vaddrOptions: vaddr.vaddrOptions,


Loading…
Cancel
Save