Unverified Commit c170115c authored by Erik Wilson's avatar Erik Wilson Committed by GitHub

Merge pull request #676 from erikwilson/go-proxy

Add go load-balancing proxy
parents 5deef130 46894ab7
package loadbalancer
import (
"encoding/json"
"io/ioutil"
"github.com/rancher/k3s/pkg/agent/util"
)
func (lb *LoadBalancer) writeConfig() error {
configOut, err := json.MarshalIndent(lb, "", " ")
if err != nil {
return err
}
if err := util.WriteFile(lb.configFile, string(configOut)); err != nil {
return err
}
return nil
}
func (lb *LoadBalancer) updateConfig() error {
writeConfig := true
if configBytes, err := ioutil.ReadFile(lb.configFile); err == nil {
config := &LoadBalancer{}
if err := json.Unmarshal(configBytes, config); err == nil {
if config.ServerURL == lb.ServerURL {
writeConfig = false
lb.setServers(config.ServerAddresses)
}
}
}
if writeConfig {
if err := lb.writeConfig(); err != nil {
return err
}
}
return nil
}
package loadbalancer
import (
"context"
"errors"
"net"
"path/filepath"
"sync"
"github.com/google/tcpproxy"
"github.com/rancher/k3s/pkg/cli/cmds"
"github.com/sirupsen/logrus"
)
type LoadBalancer struct {
mutex sync.Mutex
dialer *net.Dialer
proxy *tcpproxy.Proxy
configFile string
localAddress string
localServerURL string
originalServerAddress string
ServerURL string
ServerAddresses []string
randomServers []string
currentServerAddress string
nextServerIndex int
}
const (
serviceName = "k3s-agent-load-balancer"
)
func Setup(ctx context.Context, cfg cmds.Agent) (_lb *LoadBalancer, _err error) {
if cfg.DisableLoadBalancer {
return nil, nil
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
defer func() {
if _err != nil {
logrus.Warnf("Error starting load balancer: %s", _err)
if listener != nil {
listener.Close()
}
}
}()
if err != nil {
return nil, err
}
localAddress := listener.Addr().String()
originalServerAddress, localServerURL, err := parseURL(cfg.ServerURL, localAddress)
if err != nil {
return nil, err
}
lb := &LoadBalancer{
dialer: &net.Dialer{},
configFile: filepath.Join(cfg.DataDir, "etc", serviceName+".json"),
localAddress: localAddress,
localServerURL: localServerURL,
originalServerAddress: originalServerAddress,
ServerURL: cfg.ServerURL,
}
lb.setServers([]string{lb.originalServerAddress})
lb.proxy = &tcpproxy.Proxy{
ListenFunc: func(string, string) (net.Listener, error) {
return listener, nil
},
}
lb.proxy.AddRoute(serviceName, &tcpproxy.DialProxy{
Addr: serviceName,
DialContext: lb.dialContext,
})
if err := lb.updateConfig(); err != nil {
return nil, err
}
if err := lb.proxy.Start(); err != nil {
return nil, err
}
logrus.Infof("Running load balancer %s -> %v", lb.localAddress, lb.randomServers)
return lb, nil
}
func (lb *LoadBalancer) Update(serverAddresses []string) {
if lb == nil {
return
}
if !lb.setServers(serverAddresses) {
return
}
logrus.Infof("Updating load balancer server addresses -> %v", lb.randomServers)
if err := lb.writeConfig(); err != nil {
logrus.Warnf("Error updating load balancer config: %s", err)
}
}
func (lb *LoadBalancer) LoadBalancerServerURL() string {
if lb == nil {
return ""
}
return lb.localServerURL
}
func (lb *LoadBalancer) dialContext(ctx context.Context, network, address string) (net.Conn, error) {
startIndex := lb.nextServerIndex
for {
targetServer := lb.currentServerAddress
conn, err := lb.dialer.DialContext(ctx, network, targetServer)
if err == nil {
return conn, nil
}
logrus.Warnf("Dial error from load balancer: %s", err)
newServer, err := lb.nextServer(targetServer)
if err != nil {
return nil, err
}
if targetServer != newServer {
logrus.Warnf("Dial context in load balancer failed over to %s", newServer)
}
if ctx.Err() != nil {
return nil, ctx.Err()
}
maxIndex := len(lb.randomServers)
if startIndex > maxIndex {
startIndex = maxIndex
}
if lb.nextServerIndex == startIndex {
return nil, errors.New("all servers failed")
}
}
}
package loadbalancer
import (
"bufio"
"context"
"errors"
"fmt"
"io/ioutil"
"net"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/rancher/k3s/pkg/cli/cmds"
)
type server struct {
listener net.Listener
conns []net.Conn
prefix string
}
func createServer(prefix string) (*server, error) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
s := &server{
prefix: prefix,
listener: listener,
}
go s.serve()
return s, nil
}
func (s *server) serve() {
for {
conn, err := s.listener.Accept()
if err != nil {
return
}
s.conns = append(s.conns, conn)
go s.echo(conn)
}
}
func (s *server) close() {
s.listener.Close()
for _, conn := range s.conns {
conn.Close()
}
}
func (s *server) echo(conn net.Conn) {
for {
result, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return
}
conn.Write([]byte(s.prefix + ":" + result))
}
}
func ping(conn net.Conn) (string, error) {
fmt.Fprintf(conn, "ping\n")
result, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return "", err
}
return strings.TrimSpace(result), nil
}
func assertEqual(t *testing.T, a interface{}, b interface{}) {
if a != b {
t.Fatalf("[ %v != %v ]", a, b)
}
}
func assertNotEqual(t *testing.T, a interface{}, b interface{}) {
if a == b {
t.Fatalf("[ %v == %v ]", a, b)
}
}
func TestFailOver(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "lb-test")
if err != nil {
assertEqual(t, err, nil)
}
defer os.RemoveAll(tmpDir)
ogServe, err := createServer("og")
if err != nil {
assertEqual(t, err, nil)
}
lbServe, err := createServer("lb")
if err != nil {
assertEqual(t, err, nil)
}
cfg := cmds.Agent{
ServerURL: fmt.Sprintf("http://%s/", ogServe.listener.Addr().String()),
DataDir: tmpDir,
}
lb, err := Setup(context.Background(), cfg)
if err != nil {
assertEqual(t, err, nil)
}
parsedURL, err := url.Parse(lb.LoadBalancerServerURL())
if err != nil {
assertEqual(t, err, nil)
}
localAddress := parsedURL.Host
lb.Update([]string{lbServe.listener.Addr().String()})
conn1, err := net.Dial("tcp", localAddress)
if err != nil {
assertEqual(t, err, nil)
}
result1, err := ping(conn1)
if err != nil {
assertEqual(t, err, nil)
}
assertEqual(t, result1, "lb:ping")
lbServe.close()
_, err = ping(conn1)
assertNotEqual(t, err, nil)
conn2, err := net.Dial("tcp", localAddress)
if err != nil {
assertEqual(t, err, nil)
}
result2, err := ping(conn2)
if err != nil {
assertEqual(t, err, nil)
}
assertEqual(t, result2, "og:ping")
}
func TestFailFast(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "lb-test")
if err != nil {
assertEqual(t, err, nil)
}
defer os.RemoveAll(tmpDir)
cfg := cmds.Agent{
ServerURL: "http://127.0.0.1:-1/",
DataDir: tmpDir,
}
lb, err := Setup(context.Background(), cfg)
if err != nil {
assertEqual(t, err, nil)
}
conn, err := net.Dial("tcp", lb.localAddress)
if err != nil {
assertEqual(t, err, nil)
}
done := make(chan error)
go func() {
_, err = ping(conn)
done <- err
}()
timeout := time.After(10 * time.Millisecond)
select {
case err := <-done:
assertNotEqual(t, err, nil)
case <-timeout:
t.Fatal(errors.New("time out"))
}
}
package loadbalancer
import (
"errors"
"math/rand"
"reflect"
)
func (lb *LoadBalancer) setServers(serverAddresses []string) bool {
serverAddresses, hasOriginalServer := sortServers(serverAddresses, lb.originalServerAddress)
if len(serverAddresses) == 0 {
return false
}
lb.mutex.Lock()
defer lb.mutex.Unlock()
if reflect.DeepEqual(serverAddresses, lb.ServerAddresses) {
return false
}
lb.ServerAddresses = serverAddresses
lb.randomServers = append([]string{}, lb.ServerAddresses...)
rand.Shuffle(len(lb.randomServers), func(i, j int) {
lb.randomServers[i], lb.randomServers[j] = lb.randomServers[j], lb.randomServers[i]
})
if !hasOriginalServer {
lb.randomServers = append(lb.randomServers, lb.originalServerAddress)
}
lb.currentServerAddress = lb.randomServers[0]
lb.nextServerIndex = 1
return true
}
func (lb *LoadBalancer) nextServer(failedServer string) (string, error) {
lb.mutex.Lock()
defer lb.mutex.Unlock()
if len(lb.randomServers) == 0 {
return "", errors.New("No servers in load balancer proxy list")
}
if len(lb.randomServers) == 1 {
return lb.currentServerAddress, nil
}
if failedServer != lb.currentServerAddress {
return lb.currentServerAddress, nil
}
if lb.nextServerIndex >= len(lb.randomServers) {
lb.nextServerIndex = 0
}
lb.currentServerAddress = lb.randomServers[lb.nextServerIndex]
lb.nextServerIndex++
return lb.currentServerAddress, nil
}
package loadbalancer
import (
"errors"
"net/url"
"sort"
"strings"
)
func parseURL(serverURL, newHost string) (string, string, error) {
parsedURL, err := url.Parse(serverURL)
if err != nil {
return "", "", err
}
if parsedURL.Host == "" {
return "", "", errors.New("Initial server URL host is not defined for load balancer")
}
address := parsedURL.Host
if parsedURL.Port() == "" {
if strings.ToLower(parsedURL.Scheme) == "http" {
address += ":80"
}
if strings.ToLower(parsedURL.Scheme) == "https" {
address += ":443"
}
}
parsedURL.Host = newHost
return address, parsedURL.String(), nil
}
func sortServers(input []string, search string) ([]string, bool) {
result := []string{}
found := false
skip := map[string]bool{"": true}
for _, entry := range input {
if skip[entry] {
continue
}
if search == entry {
found = true
}
skip[entry] = true
result = append(result, entry)
}
sort.Strings(result)
return result, found
}
...@@ -12,6 +12,7 @@ import ( ...@@ -12,6 +12,7 @@ import (
"github.com/rancher/k3s/pkg/agent/config" "github.com/rancher/k3s/pkg/agent/config"
"github.com/rancher/k3s/pkg/agent/containerd" "github.com/rancher/k3s/pkg/agent/containerd"
"github.com/rancher/k3s/pkg/agent/flannel" "github.com/rancher/k3s/pkg/agent/flannel"
"github.com/rancher/k3s/pkg/agent/loadbalancer"
"github.com/rancher/k3s/pkg/agent/syssetup" "github.com/rancher/k3s/pkg/agent/syssetup"
"github.com/rancher/k3s/pkg/agent/tunnel" "github.com/rancher/k3s/pkg/agent/tunnel"
"github.com/rancher/k3s/pkg/cli/cmds" "github.com/rancher/k3s/pkg/cli/cmds"
...@@ -21,7 +22,7 @@ import ( ...@@ -21,7 +22,7 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func run(ctx context.Context, cfg cmds.Agent) error { func run(ctx context.Context, cfg cmds.Agent, lb *loadbalancer.LoadBalancer) error {
nodeConfig := config.Get(ctx, cfg) nodeConfig := config.Get(ctx, cfg)
if err := config.HostnameCheck(cfg); err != nil { if err := config.HostnameCheck(cfg); err != nil {
...@@ -47,7 +48,7 @@ func run(ctx context.Context, cfg cmds.Agent) error { ...@@ -47,7 +48,7 @@ func run(ctx context.Context, cfg cmds.Agent) error {
return err return err
} }
if err := tunnel.Setup(ctx, nodeConfig); err != nil { if err := tunnel.Setup(ctx, nodeConfig, lb.Update); err != nil {
return err return err
} }
...@@ -77,11 +78,20 @@ func Run(ctx context.Context, cfg cmds.Agent) error { ...@@ -77,11 +78,20 @@ func Run(ctx context.Context, cfg cmds.Agent) error {
} }
cfg.DataDir = filepath.Join(cfg.DataDir, "agent") cfg.DataDir = filepath.Join(cfg.DataDir, "agent")
os.MkdirAll(cfg.DataDir, 0700)
if cfg.ClusterSecret != "" { if cfg.ClusterSecret != "" {
cfg.Token = "K10node:" + cfg.ClusterSecret cfg.Token = "K10node:" + cfg.ClusterSecret
} }
lb, err := loadbalancer.Setup(ctx, cfg)
if err != nil {
return err
}
if lb != nil {
cfg.ServerURL = lb.LoadBalancerServerURL()
}
for { for {
tmpFile, err := clientaccess.AgentAccessInfoToTempKubeConfig("", cfg.ServerURL, cfg.Token) tmpFile, err := clientaccess.AgentAccessInfoToTempKubeConfig("", cfg.ServerURL, cfg.Token)
if err != nil { if err != nil {
...@@ -97,8 +107,7 @@ func Run(ctx context.Context, cfg cmds.Agent) error { ...@@ -97,8 +107,7 @@ func Run(ctx context.Context, cfg cmds.Agent) error {
break break
} }
os.MkdirAll(cfg.DataDir, 0700) return run(ctx, cfg, lb)
return run(ctx, cfg)
} }
func validate() error { func validate() error {
......
...@@ -53,7 +53,7 @@ func getAddresses(endpoint *v1.Endpoints) []string { ...@@ -53,7 +53,7 @@ func getAddresses(endpoint *v1.Endpoints) []string {
return serverAddresses return serverAddresses
} }
func Setup(ctx context.Context, config *config.Node) error { func Setup(ctx context.Context, config *config.Node, onChange func([]string)) error {
restConfig, err := clientcmd.BuildConfigFromFlags("", config.AgentConfig.KubeConfigNode) restConfig, err := clientcmd.BuildConfigFromFlags("", config.AgentConfig.KubeConfigNode)
if err != nil { if err != nil {
return err return err
...@@ -74,6 +74,9 @@ func Setup(ctx context.Context, config *config.Node) error { ...@@ -74,6 +74,9 @@ func Setup(ctx context.Context, config *config.Node) error {
endpoint, _ := client.CoreV1().Endpoints("default").Get("kubernetes", metav1.GetOptions{}) endpoint, _ := client.CoreV1().Endpoints("default").Get("kubernetes", metav1.GetOptions{})
if endpoint != nil { if endpoint != nil {
addresses = getAddresses(endpoint) addresses = getAddresses(endpoint)
if onChange != nil {
onChange(addresses)
}
} }
disconnect := map[string]context.CancelFunc{} disconnect := map[string]context.CancelFunc{}
...@@ -120,6 +123,9 @@ func Setup(ctx context.Context, config *config.Node) error { ...@@ -120,6 +123,9 @@ func Setup(ctx context.Context, config *config.Node) error {
} }
addresses = newAddresses addresses = newAddresses
logrus.Infof("Tunnel endpoint watch event: %v", addresses) logrus.Infof("Tunnel endpoint watch event: %v", addresses)
if onChange != nil {
onChange(addresses)
}
validEndpoint := map[string]bool{} validEndpoint := map[string]bool{}
......
...@@ -11,6 +11,7 @@ type Agent struct { ...@@ -11,6 +11,7 @@ type Agent struct {
Token string Token string
TokenFile string TokenFile string
ServerURL string ServerURL string
DisableLoadBalancer bool
ResolvConf string ResolvConf string
DataDir string DataDir string
NodeIP string NodeIP string
......
...@@ -214,6 +214,7 @@ func run(app *cli.Context, cfg *cmds.Server) error { ...@@ -214,6 +214,7 @@ func run(app *cli.Context, cfg *cmds.Server) error {
agentConfig.ServerURL = url agentConfig.ServerURL = url
agentConfig.Token = token agentConfig.Token = token
agentConfig.Labels = append(agentConfig.Labels, "node-role.kubernetes.io/master=true") agentConfig.Labels = append(agentConfig.Labels, "node-role.kubernetes.io/master=true")
agentConfig.DisableLoadBalancer = true
return agent.Run(ctx, agentConfig) return agent.Run(ctx, agentConfig)
} }
......
...@@ -125,6 +125,8 @@ import: ...@@ -125,6 +125,8 @@ import:
version: v1.0.21 version: v1.0.21
- package: github.com/google/gofuzz - package: github.com/google/gofuzz
version: 44d81051d367757e1c7c6a5a86423ece9afcf63c version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
- package: github.com/google/tcpproxy
version: dfa16c61dad2b18a385dfb351adf71566720535b
- package: github.com/googleapis/gnostic - package: github.com/googleapis/gnostic
version: 0c5108395e2debce0d731cf0287ddf7242066aba version: 0c5108395e2debce0d731cf0287ddf7242066aba
- package: github.com/gorilla/mux - package: github.com/gorilla/mux
......
...@@ -31,6 +31,7 @@ gopkg.in/freddierice/go-losetup.v1 fc9adea44124401d8bfef3a97eaf61b5d44cc2c6 ...@@ -31,6 +31,7 @@ gopkg.in/freddierice/go-losetup.v1 fc9adea44124401d8bfef3a97eaf61b5d44cc2c6
github.com/urfave/cli 8e01ec4cd3e2d84ab2fe90d8210528ffbb06d8ff github.com/urfave/cli 8e01ec4cd3e2d84ab2fe90d8210528ffbb06d8ff
golang.org/x/sync 450f422ab23cf9881c94e2db30cac0eb1b7cf80c golang.org/x/sync 450f422ab23cf9881c94e2db30cac0eb1b7cf80c
github.com/docker/docker c12f09bf99b54f274a5ae241dd154fa74020cbab github.com/docker/docker c12f09bf99b54f274a5ae241dd154fa74020cbab
github.com/google/tcpproxy dfa16c61dad2b18a385dfb351adf71566720535b
# flannel # flannel
github.com/golang/glog 23def4e6c14b4da8ac2ed8007337bc5eb5007998 github.com/golang/glog 23def4e6c14b4da8ac2ed8007337bc5eb5007998
......
language: go
go:
- 1.8
- tip
os:
- linux
install:
- go get github.com/golang/lint/golint
before_script:
script:
- go get -t ./...
- go build ./...
- go test ./...
- go vet ./...
- golint -set_exit_status .
jobs:
include:
- stage: deploy
go: 1.8
install:
- gem install fpm
script:
- go build ./cmd/tlsrouter
- fpm -s dir -t deb -n tlsrouter -v $(date '+%Y%m%d%H%M%S')
--license Apache2
--vendor "David Anderson <dave@natulte.net>"
--maintainer "David Anderson <dave@natulte.net>"
--description "TLS SNI router"
--url "https://github.com/google/tlsrouter"
./tlsrouter=/usr/bin/tlsrouter
./systemd/tlsrouter.service=/lib/systemd/system/tlsrouter.service
deploy:
- provider: packagecloud
repository: tlsrouter
username: danderson
dist: debian/stretch
skip_cleanup: true
on:
branch: master
token:
secure: gNU3o70EU4oYeIS6pr0K5oLMGqqxrcf41EOv6c/YoHPVdV6Cx4j9NW0/ISgu6a1/Xf2NgWKT5BWwLpAuhmGdALuOz1Ah//YBWd9N8mGHGaC6RpOPDU8/9NkQdBEmjEH9sgX4PNOh1KQ7d7O0OH0g8RqJlJa0MkUYbTtN6KJ29oiUXxKmZM4D/iWB8VonKOnrtx1NwQL8jL8imZyEV/1fknhDwumz2iKeU1le4Neq9zkxwICMLUonmgphlrp+SDb1EOoHxT6cn51bqBQtQUplfC4dN4OQU/CPqE9E1N1noibvN29YA93qfcrjD3I95KT9wzq+3B6he33+kb0Gz+Cj5ypGy4P85l7TuX4CtQg0U3NAlJCk32IfsdjK+o47pdmADij9IIb9yKt+g99FMERkJJY5EInqEsxHlW/vNF5OqQCmpiHstZL4R2XaHEsWh6j77npnjjC1Aea8xZTWr8PTsbSzVkbG7bTmFpZoPH8eEmr4GNuw5gnbi6D1AJDjcA+UdY9s5qZNpzuWOqfhOFxL+zUW+8sHBvcoFw3R+pwHECs2LCL1c0xAC1LtNUnmW/gnwHavtvKkzErjR1P8Xl7obCbeChJjp+b/BcFYlNACldZcuzBAPyPwIdlWVyUonL4bm63upfMEEShiAIDDJ21y7fjsQK7CfPA7g25bpyo+hV8=
- provider: script
on:
branch: master
script: go run scripts/prune_old_versions.go -user=danderson -repo=tlsrouter -distro=debian -version=stretch -package=tlsrouter -arch=amd64 -limit=2
env:
# Packagecloud API key, for prune_old_versions.go
- secure: "SRcNwt+45QyPS1w9aGxMg9905Y6d9w4mBM29G6iTTnUB5nD7cAk4m+tf834knGSobVXlWcRnTDW8zrHdQ9yX22dPqCpH5qE+qzTmIvxRHrVJRMmPeYvligJ/9jYfHgQbvuRT8cUpIcpCQAla6rw8nXfKTOE3h8XqMP2hdc3DTVOu2HCfKCNco1tJ7is+AIAnFV2Wpsbb3ZsdKFvHvi2RKUfFaX61J1GNt2/XJIlZs8jC6Y1IAC+ftjql9UsAE/WjZ9fL0Ww1b9/LBIIGHXWI3HpVv9WvlhhIxIlJgOVjmU2lbSuj2w/EBDJ9cd1Qe+wJkT3yKzE1NRsNScVjGg+Ku5igJu/XXuaHkIX01+15BqgPduBYRL0atiNQDhqgBiSyVhXZBX9vsgsp0bgpKaBSF++CV18Q9dara8aljqqS33M3imO3I8JmXU10944QA9Wvu7pCYuIzXxhINcDXRvqxBqz5LnFJGwnGqngTrOCSVS2xn7Y+sjmhe1n5cPCEISlozfa9mPYPvMPp8zg3TbATOOM8CVfcpaNscLqa/+SExN3zMwSanjNKrBgoaQcBzGW5mIgSPxhXkWikBgapiEN7+2Y032Lhqdb9dYjH+EuwcnofspDjjMabWxnuJaln+E3/9vZi2ooQrBEtvymUTy4VMSnqwIX5bU7nPdIuQycdWhk="
Contributions are welcome by pull request.
You need to sign the Google Contributor License Agreement before your
contributions can be accepted. You can find the individual and organization
level CLAs here:
Individual: https://cla.developers.google.com/about/google-individual
Organization: https://cla.developers.google.com/about/google-corporate
# tcpproxy
For library usage, see https://godoc.org/github.com/google/tcpproxy/
For CLI usage, see https://github.com/google/tcpproxy/blob/master/cmd/tlsrouter/README.md
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcpproxy
import (
"bufio"
"bytes"
"context"
"net/http"
)
// AddHTTPHostRoute appends a route to the ipPort listener that
// routes to dest if the incoming HTTP/1.x Host header name is
// httpHost. If it doesn't match, rule processing continues for any
// additional routes on ipPort.
//
// The ipPort is any valid net.Listen TCP address.
func (p *Proxy) AddHTTPHostRoute(ipPort, httpHost string, dest Target) {
p.AddHTTPHostMatchRoute(ipPort, equals(httpHost), dest)
}
// AddHTTPHostMatchRoute appends a route to the ipPort listener that
// routes to dest if the incoming HTTP/1.x Host header name is
// accepted by matcher. If it doesn't match, rule processing continues
// for any additional routes on ipPort.
//
// The ipPort is any valid net.Listen TCP address.
func (p *Proxy) AddHTTPHostMatchRoute(ipPort string, match Matcher, dest Target) {
p.addRoute(ipPort, httpHostMatch{match, dest})
}
type httpHostMatch struct {
matcher Matcher
target Target
}
func (m httpHostMatch) match(br *bufio.Reader) (Target, string) {
hh := httpHostHeader(br)
if m.matcher(context.TODO(), hh) {
return m.target, hh
}
return nil, ""
}
// httpHostHeader returns the HTTP Host header from br without
// consuming any of its bytes. It returns "" if it can't find one.
func httpHostHeader(br *bufio.Reader) string {
const maxPeek = 4 << 10
peekSize := 0
for {
peekSize++
if peekSize > maxPeek {
b, _ := br.Peek(br.Buffered())
return httpHostHeaderFromBytes(b)
}
b, err := br.Peek(peekSize)
if n := br.Buffered(); n > peekSize {
b, _ = br.Peek(n)
peekSize = n
}
if len(b) > 0 {
if b[0] < 'A' || b[0] > 'Z' {
// Doesn't look like an HTTP verb
// (GET, POST, etc).
return ""
}
if bytes.Index(b, crlfcrlf) != -1 || bytes.Index(b, lflf) != -1 {
req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(b)))
if err != nil {
return ""
}
if len(req.Header["Host"]) > 1 {
// TODO(bradfitz): what does
// ReadRequest do if there are
// multiple Host headers?
return ""
}
return req.Host
}
}
if err != nil {
return httpHostHeaderFromBytes(b)
}
}
}
var (
lfHostColon = []byte("\nHost:")
lfhostColon = []byte("\nhost:")
crlf = []byte("\r\n")
lf = []byte("\n")
crlfcrlf = []byte("\r\n\r\n")
lflf = []byte("\n\n")
)
func httpHostHeaderFromBytes(b []byte) string {
if i := bytes.Index(b, lfHostColon); i != -1 {
return string(bytes.TrimSpace(untilEOL(b[i+len(lfHostColon):])))
}
if i := bytes.Index(b, lfhostColon); i != -1 {
return string(bytes.TrimSpace(untilEOL(b[i+len(lfhostColon):])))
}
return ""
}
// untilEOL returns v, truncated before the first '\n' byte, if any.
// The returned slice may include a '\r' at the end.
func untilEOL(v []byte) []byte {
if i := bytes.IndexByte(v, '\n'); i != -1 {
return v[:i]
}
return v
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcpproxy
import (
"io"
"net"
"sync"
)
// TargetListener implements both net.Listener and Target.
// Matched Targets become accepted connections.
type TargetListener struct {
Address string // Address is the string reported by TargetListener.Addr().String().
mu sync.Mutex
cond *sync.Cond
closed bool
nextConn net.Conn
}
var (
_ net.Listener = (*TargetListener)(nil)
_ Target = (*TargetListener)(nil)
)
func (tl *TargetListener) lock() {
tl.mu.Lock()
if tl.cond == nil {
tl.cond = sync.NewCond(&tl.mu)
}
}
type tcpAddr string
func (a tcpAddr) Network() string { return "tcp" }
func (a tcpAddr) String() string { return string(a) }
// Addr returns the listener's Address field as a net.Addr.
func (tl *TargetListener) Addr() net.Addr { return tcpAddr(tl.Address) }
// Close stops listening for new connections. All new connections
// routed to this listener will be closed. Already accepted
// connections are not closed.
func (tl *TargetListener) Close() error {
tl.lock()
if tl.closed {
tl.mu.Unlock()
return nil
}
tl.closed = true
tl.mu.Unlock()
tl.cond.Broadcast()
return nil
}
// HandleConn implements the Target interface. It blocks until tl is
// closed or another goroutine has called Accept and received c.
func (tl *TargetListener) HandleConn(c net.Conn) {
tl.lock()
defer tl.mu.Unlock()
for tl.nextConn != nil && !tl.closed {
tl.cond.Wait()
}
if tl.closed {
c.Close()
return
}
tl.nextConn = c
tl.cond.Broadcast() // Signal might be sufficient; verify.
for tl.nextConn == c && !tl.closed {
tl.cond.Wait()
}
if tl.closed {
c.Close()
return
}
}
// Accept implements the Accept method in the net.Listener interface.
func (tl *TargetListener) Accept() (net.Conn, error) {
tl.lock()
for tl.nextConn == nil && !tl.closed {
tl.cond.Wait()
}
if tl.closed {
tl.mu.Unlock()
return nil, io.EOF
}
c := tl.nextConn
tl.nextConn = nil
tl.mu.Unlock()
tl.cond.Broadcast() // Signal might be sufficient; verify.
return c, nil
}
// Copyright 2017 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tcpproxy
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"io"
"net"
"strings"
)
// AddSNIRoute appends a route to the ipPort listener that routes to
// dest if the incoming TLS SNI server name is sni. If it doesn't
// match, rule processing continues for any additional routes on
// ipPort.
//
// By default, the proxy will route all ACME tls-sni-01 challenges
// received on ipPort to all SNI dests. You can disable ACME routing
// with AddStopACMESearch.
//
// The ipPort is any valid net.Listen TCP address.
func (p *Proxy) AddSNIRoute(ipPort, sni string, dest Target) {
p.AddSNIMatchRoute(ipPort, equals(sni), dest)
}
// AddSNIMatchRoute appends a route to the ipPort listener that routes
// to dest if the incoming TLS SNI server name is accepted by
// matcher. If it doesn't match, rule processing continues for any
// additional routes on ipPort.
//
// By default, the proxy will route all ACME tls-sni-01 challenges
// received on ipPort to all SNI dests. You can disable ACME routing
// with AddStopACMESearch.
//
// The ipPort is any valid net.Listen TCP address.
func (p *Proxy) AddSNIMatchRoute(ipPort string, matcher Matcher, dest Target) {
cfg := p.configFor(ipPort)
if !cfg.stopACME {
if len(cfg.acmeTargets) == 0 {
p.addRoute(ipPort, &acmeMatch{cfg})
}
cfg.acmeTargets = append(cfg.acmeTargets, dest)
}
p.addRoute(ipPort, sniMatch{matcher, dest})
}
// AddStopACMESearch prevents ACME probing of subsequent SNI routes.
// Any ACME challenges on ipPort for SNI routes previously added
// before this call will still be proxied to all possible SNI
// backends.
func (p *Proxy) AddStopACMESearch(ipPort string) {
p.configFor(ipPort).stopACME = true
}
type sniMatch struct {
matcher Matcher
target Target
}
func (m sniMatch) match(br *bufio.Reader) (Target, string) {
sni := clientHelloServerName(br)
if m.matcher(context.TODO(), sni) {
return m.target, sni
}
return nil, ""
}
// acmeMatch matches "*.acme.invalid" ACME tls-sni-01 challenges and
// searches for a Target in cfg.acmeTargets that has the challenge
// response.
type acmeMatch struct {
cfg *config
}
func (m *acmeMatch) match(br *bufio.Reader) (Target, string) {
sni := clientHelloServerName(br)
if !strings.HasSuffix(sni, ".acme.invalid") {
return nil, ""
}
// TODO: cache. ACME issuers will hit multiple times in a short
// burst for each issuance event. A short TTL cache + singleflight
// should have an excellent hit rate.
// TODO: maybe an acme-specific timeout as well?
// TODO: plumb context upwards?
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan Target, len(m.cfg.acmeTargets))
for _, target := range m.cfg.acmeTargets {
go tryACME(ctx, ch, target, sni)
}
for range m.cfg.acmeTargets {
if target := <-ch; target != nil {
return target, sni
}
}
// No target was happy with the provided challenge.
return nil, ""
}
func tryACME(ctx context.Context, ch chan<- Target, dest Target, sni string) {
var ret Target
defer func() { ch <- ret }()
conn, targetConn := net.Pipe()
defer conn.Close()
go dest.HandleConn(targetConn)
deadline, ok := ctx.Deadline()
if ok {
conn.SetDeadline(deadline)
}
client := tls.Client(conn, &tls.Config{
ServerName: sni,
InsecureSkipVerify: true,
})
if err := client.Handshake(); err != nil {
// TODO: log?
return
}
certs := client.ConnectionState().PeerCertificates
if len(certs) == 0 {
// TODO: log?
return
}
// acme says the first cert offered by the server must match the
// challenge hostname.
if err := certs[0].VerifyHostname(sni); err != nil {
// TODO: log?
return
}
// Target presented what looks like a valid challenge
// response, send it back to the matcher.
ret = dest
}
// clientHelloServerName returns the SNI server name inside the TLS ClientHello,
// without consuming any bytes from br.
// On any error, the empty string is returned.
func clientHelloServerName(br *bufio.Reader) (sni string) {
const recordHeaderLen = 5
hdr, err := br.Peek(recordHeaderLen)
if err != nil {
return ""
}
const recordTypeHandshake = 0x16
if hdr[0] != recordTypeHandshake {
return "" // Not TLS.
}
recLen := int(hdr[3])<<8 | int(hdr[4]) // ignoring version in hdr[1:3]
helloBytes, err := br.Peek(recordHeaderLen + recLen)
if err != nil {
return ""
}
tls.Server(sniSniffConn{r: bytes.NewReader(helloBytes)}, &tls.Config{
GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
sni = hello.ServerName
return nil, nil
},
}).Handshake()
return
}
// sniSniffConn is a net.Conn that reads from r, fails on Writes,
// and crashes otherwise.
type sniSniffConn struct {
r io.Reader
net.Conn // nil; crash on any unexpected use
}
func (c sniSniffConn) Read(p []byte) (int, error) { return c.r.Read(p) }
func (sniSniffConn) Write(p []byte) (int, error) { return 0, io.EOF }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment