├── .gitignore ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── config.go ├── k8sniff.json.example ├── kubernetes ├── example │ ├── bar.yaml │ ├── foo.yaml │ ├── k8sniff.json │ └── tcp-ingress.yaml ├── k8sniff-configmap.yaml ├── k8sniff-deployment.yaml ├── k8sniff-namespace.yaml └── k8sniff-svc.yaml ├── logo └── logo.png ├── main.go ├── metrics ├── backends.go ├── connections.go ├── doc.go ├── errors.go ├── init.go └── serve.go ├── parser └── parser.go ├── server.go └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *swp 4 | sniff 5 | .wercker 6 | vendor 7 | -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | name = "github.com/PuerkitoBio/purell" 6 | packages = ["."] 7 | revision = "8a290539e2e8629dbc4e6bad948158f790ec31f4" 8 | version = "v1.0.0" 9 | 10 | [[projects]] 11 | name = "github.com/PuerkitoBio/urlesc" 12 | packages = ["."] 13 | revision = "5bd2802263f21d8788851d5305584c82a5c75d7e" 14 | 15 | [[projects]] 16 | branch = "master" 17 | name = "github.com/beorn7/perks" 18 | packages = ["quantile"] 19 | revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" 20 | 21 | [[projects]] 22 | name = "github.com/davecgh/go-spew" 23 | packages = ["spew"] 24 | revision = "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d" 25 | 26 | [[projects]] 27 | name = "github.com/emicklei/go-restful" 28 | packages = [".","log"] 29 | revision = "ff4f55a206334ef123e4f79bbf348980da81ca46" 30 | 31 | [[projects]] 32 | name = "github.com/emicklei/go-restful-swagger12" 33 | packages = ["."] 34 | revision = "dcef7f55730566d41eae5db10e7d6981829720f6" 35 | version = "1.0.1" 36 | 37 | [[projects]] 38 | name = "github.com/ghodss/yaml" 39 | packages = ["."] 40 | revision = "73d445a93680fa1a78ae23a5839bad48f32ba1ee" 41 | 42 | [[projects]] 43 | name = "github.com/go-openapi/jsonpointer" 44 | packages = ["."] 45 | revision = "46af16f9f7b149af66e5d1bd010e3574dc06de98" 46 | 47 | [[projects]] 48 | name = "github.com/go-openapi/jsonreference" 49 | packages = ["."] 50 | revision = "13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272" 51 | 52 | [[projects]] 53 | name = "github.com/go-openapi/spec" 54 | packages = ["."] 55 | revision = "6aced65f8501fe1217321abf0749d354824ba2ff" 56 | 57 | [[projects]] 58 | name = "github.com/go-openapi/swag" 59 | packages = ["."] 60 | revision = "1d0bd113de87027671077d3c71eb3ac5d7dbba72" 61 | 62 | [[projects]] 63 | name = "github.com/gogo/protobuf" 64 | packages = ["proto","sortkeys"] 65 | revision = "c0656edd0d9eab7c66d1eb0c568f9039345796f7" 66 | 67 | [[projects]] 68 | branch = "master" 69 | name = "github.com/golang/glog" 70 | packages = ["."] 71 | revision = "23def4e6c14b4da8ac2ed8007337bc5eb5007998" 72 | 73 | [[projects]] 74 | name = "github.com/golang/protobuf" 75 | packages = ["proto","ptypes","ptypes/any","ptypes/duration","ptypes/timestamp"] 76 | revision = "4bd1920723d7b7c925de087aa32e2187708897f7" 77 | 78 | [[projects]] 79 | name = "github.com/google/gofuzz" 80 | packages = ["."] 81 | revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c" 82 | 83 | [[projects]] 84 | name = "github.com/googleapis/gnostic" 85 | packages = ["OpenAPIv2","compiler","extensions"] 86 | revision = "68f4ded48ba9414dab2ae69b3f0d69971da73aa5" 87 | 88 | [[projects]] 89 | name = "github.com/hashicorp/golang-lru" 90 | packages = [".","simplelru"] 91 | revision = "a0d98a5f288019575c6d1f4bb1573fef2d1fcdc4" 92 | 93 | [[projects]] 94 | branch = "master" 95 | name = "github.com/howeyc/gopass" 96 | packages = ["."] 97 | revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" 98 | 99 | [[projects]] 100 | name = "github.com/imdario/mergo" 101 | packages = ["."] 102 | revision = "6633656539c1639d9d78127b7d47c622b5d7b6dc" 103 | 104 | [[projects]] 105 | branch = "master" 106 | name = "github.com/juju/ratelimit" 107 | packages = ["."] 108 | revision = "5b9ff866471762aa2ab2dced63c9fb6f53921342" 109 | 110 | [[projects]] 111 | name = "github.com/mailru/easyjson" 112 | packages = ["buffer","jlexer","jwriter"] 113 | revision = "d5b7844b561a7bc640052f1b935f7b800330d7e0" 114 | 115 | [[projects]] 116 | branch = "master" 117 | name = "github.com/matttproud/golang_protobuf_extensions" 118 | packages = ["pbutil"] 119 | revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" 120 | 121 | [[projects]] 122 | name = "github.com/prometheus/client_golang" 123 | packages = ["prometheus","prometheus/promhttp"] 124 | revision = "26b897001974f2b4ee6688377873e4d6f61d533c" 125 | 126 | [[projects]] 127 | branch = "master" 128 | name = "github.com/prometheus/client_model" 129 | packages = ["go"] 130 | revision = "6f3806018612930941127f2a7c6c453ba2c527d2" 131 | 132 | [[projects]] 133 | name = "github.com/prometheus/common" 134 | packages = ["expfmt","internal/bitbucket.org/ww/goautoneg","model"] 135 | revision = "3e6a7635bac6573d43f49f97b47eb9bda195dba8" 136 | 137 | [[projects]] 138 | branch = "master" 139 | name = "github.com/prometheus/procfs" 140 | packages = [".","xfs"] 141 | revision = "e645f4e5aaa8506fc71d6edbc5c4ff02c04c46f2" 142 | 143 | [[projects]] 144 | name = "github.com/spf13/pflag" 145 | packages = ["."] 146 | revision = "9ff6c6923cfffbcd502984b8e0c80539a94968b7" 147 | 148 | [[projects]] 149 | name = "github.com/ugorji/go" 150 | packages = ["codec"] 151 | revision = "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74" 152 | 153 | [[projects]] 154 | name = "golang.org/x/crypto" 155 | packages = ["ssh/terminal"] 156 | revision = "d172538b2cfce0c13cee31e647d0367aa8cd2486" 157 | 158 | [[projects]] 159 | name = "golang.org/x/net" 160 | packages = ["http2","http2/hpack","idna","lex/httplex"] 161 | revision = "f2499483f923065a842d38eb4c7f1927e6fc6e6d" 162 | 163 | [[projects]] 164 | name = "golang.org/x/sys" 165 | packages = ["unix"] 166 | revision = "8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9" 167 | 168 | [[projects]] 169 | name = "golang.org/x/text" 170 | packages = ["cases","internal/gen","internal/tag","internal/triegen","internal/ucd","language","runes","secure/bidirule","secure/precis","transform","unicode/bidi","unicode/cldr","unicode/norm","unicode/rangetable","width"] 171 | revision = "2910a502d2bf9e43193af9d68ca516529614eed3" 172 | 173 | [[projects]] 174 | name = "gopkg.in/inf.v0" 175 | packages = ["."] 176 | revision = "3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4" 177 | version = "v0.9.0" 178 | 179 | [[projects]] 180 | name = "gopkg.in/yaml.v2" 181 | packages = ["."] 182 | revision = "53feefa2559fb8dfa8d81baad31be332c97d6c77" 183 | 184 | [[projects]] 185 | name = "k8s.io/api" 186 | packages = ["admissionregistration/v1alpha1","apps/v1beta1","authentication/v1","authentication/v1beta1","authorization/v1","authorization/v1beta1","autoscaling/v1","autoscaling/v2alpha1","batch/v1","batch/v2alpha1","certificates/v1beta1","core/v1","extensions/v1beta1","networking/v1","policy/v1beta1","rbac/v1alpha1","rbac/v1beta1","settings/v1alpha1","storage/v1","storage/v1beta1"] 187 | revision = "4fe9229aaa9d704f8a2a21cdcd50de2bbb6e1b57" 188 | 189 | [[projects]] 190 | name = "k8s.io/apimachinery" 191 | packages = ["pkg/api/equality","pkg/api/errors","pkg/api/meta","pkg/api/resource","pkg/apis/meta/v1","pkg/apis/meta/v1/unstructured","pkg/apis/meta/v1alpha1","pkg/conversion","pkg/conversion/queryparams","pkg/conversion/unstructured","pkg/fields","pkg/labels","pkg/openapi","pkg/runtime","pkg/runtime/schema","pkg/runtime/serializer","pkg/runtime/serializer/json","pkg/runtime/serializer/protobuf","pkg/runtime/serializer/recognizer","pkg/runtime/serializer/streaming","pkg/runtime/serializer/versioning","pkg/selection","pkg/types","pkg/util/cache","pkg/util/clock","pkg/util/diff","pkg/util/errors","pkg/util/framer","pkg/util/intstr","pkg/util/json","pkg/util/net","pkg/util/runtime","pkg/util/sets","pkg/util/validation","pkg/util/validation/field","pkg/util/wait","pkg/util/yaml","pkg/version","pkg/watch","third_party/forked/golang/reflect"] 192 | revision = "8a1a257c3a3503c77f25e5802e96e89a2a11ad61" 193 | 194 | [[projects]] 195 | branch = "master" 196 | name = "k8s.io/client-go" 197 | packages = ["discovery","kubernetes","kubernetes/scheme","kubernetes/typed/admissionregistration/v1alpha1","kubernetes/typed/apps/v1beta1","kubernetes/typed/authentication/v1","kubernetes/typed/authentication/v1beta1","kubernetes/typed/authorization/v1","kubernetes/typed/authorization/v1beta1","kubernetes/typed/autoscaling/v1","kubernetes/typed/autoscaling/v2alpha1","kubernetes/typed/batch/v1","kubernetes/typed/batch/v2alpha1","kubernetes/typed/certificates/v1beta1","kubernetes/typed/core/v1","kubernetes/typed/extensions/v1beta1","kubernetes/typed/networking/v1","kubernetes/typed/policy/v1beta1","kubernetes/typed/rbac/v1alpha1","kubernetes/typed/rbac/v1beta1","kubernetes/typed/settings/v1alpha1","kubernetes/typed/storage/v1","kubernetes/typed/storage/v1beta1","pkg/api/v1/ref","pkg/version","rest","rest/watch","tools/auth","tools/cache","tools/clientcmd","tools/clientcmd/api","tools/clientcmd/api/latest","tools/clientcmd/api/v1","tools/metrics","transport","util/cert","util/flowcontrol","util/homedir","util/integer"] 198 | revision = "d03136c067423d216274eff98f834e385bec6225" 199 | 200 | [solve-meta] 201 | analyzer-name = "dep" 202 | analyzer-version = 1 203 | inputs-digest = "be6b91d6d27b60f1350837945b9e0fc9c5713af050837bbe6b5d180eb5231d5a" 204 | solver-name = "gps-cdcl" 205 | solver-version = 1 206 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | 2 | # Gopkg.toml example 3 | # 4 | # Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md 5 | # for detailed Gopkg.toml documentation. 6 | # 7 | # required = ["github.com/user/thing/cmd/thing"] 8 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 9 | # 10 | # [[constraint]] 11 | # name = "github.com/user/project" 12 | # version = "1.0.0" 13 | # 14 | # [[constraint]] 15 | # name = "github.com/user/project2" 16 | # branch = "dev" 17 | # source = "github.com/myfork/project2" 18 | # 19 | # [[override]] 20 | # name = "github.com/x/y" 21 | # version = "2.4.0" 22 | 23 | 24 | [[constraint]] 25 | name = "github.com/golang/glog" 26 | 27 | [[constraint]] 28 | name = "github.com/prometheus/client_golang" 29 | 30 | [[constraint]] 31 | branch = "master" 32 | name = "k8s.io/client-go" 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | K8SNIff 2 | 3 | K8SNIff - tcp ingress controller with SNI support 4 | ===== 5 | 6 | K8SNIff is a small ingress server that will accept incoming TLS connections and parse 7 | TLS Client Hello messages for the SNI Extension. If one is found, we'll go 8 | ahead and forward that connection to a Kubernetes service with a matching Ingress resource. 9 | 10 | sniff config 11 | ------------ 12 | 13 | The following config will K8SNIff listen on port `8443` and listen on Ingress resources 14 | 15 | ```json 16 | { 17 | "bind": { 18 | "host": "localhost", 19 | "port": 8443 20 | }, 21 | "kubernetes": {} 22 | } 23 | 24 | ``` 25 | The example ingress connect any requests to `foo` to service `foo` with port `443` and any requests to `bar` to service `bar` with port `443`. If nothing matches this, it will send the traffic to the default backend with the service `bar` on port `443`. 26 | 27 | ```yaml 28 | apiVersion: extensions/v1beta1 29 | kind: Ingress 30 | metadata: 31 | name: tcp 32 | annotations: 33 | kubernetes.io/ingress.class: k8sniff 34 | spec: 35 | backend: 36 | serviceName: bar 37 | servicePort: 443 38 | rules: 39 | - host: foo 40 | http: 41 | paths: 42 | - backend: 43 | serviceName: foo 44 | servicePort: 443 45 | - host: bar 46 | http: 47 | paths: 48 | - backend: 49 | serviceName: bar 50 | servicePort: 443 51 | ``` 52 | 53 | The requested domain name are interpreted as regular expressions. Each server and name will be checked in the order they appear in the file, stopping with the first match. If there is no match, then the request is sent to the first server with default `backend` set. 54 | 55 | using the parser 56 | ---------------- 57 | 58 | ``` 59 | import ( 60 | "fmt" 61 | 62 | " kubermatic/k8sniff/parser" 63 | ) 64 | 65 | func main() { 66 | listener, err := net.Listen("tcp", "localhost:2222") 67 | if err != nil { 68 | return err 69 | } 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | /* {{{ Copyright (c) Paul R. Tagliamonte , 2015 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. }}} */ 20 | 21 | package main 22 | 23 | import ( 24 | "encoding/json" 25 | "errors" 26 | "fmt" 27 | "os" 28 | "strings" 29 | "sync" 30 | 31 | "github.com/golang/glog" 32 | 33 | "k8s.io/client-go/kubernetes" 34 | "k8s.io/client-go/tools/cache" 35 | ) 36 | 37 | const ( 38 | maxTCPPort = 65535 39 | ) 40 | 41 | type Config struct { 42 | Bind Bind 43 | Metrics Metrics 44 | Servers []Server 45 | Kubernetes *Kubernetes 46 | proxy *Proxy 47 | lock sync.Mutex 48 | 49 | serviceController cache.Controller 50 | serviceStore cache.Store 51 | ingressController cache.Controller 52 | ingressStore cache.Store 53 | } 54 | 55 | // Valid returns an if the config is invalid 56 | func (c *Config) Valid() error { 57 | if len(c.Servers) > 0 && c.Kubernetes != nil { 58 | return errors.New("Cannot set .Servers and .Kubernetes in config file") 59 | } 60 | 61 | if err := c.Metrics.Valid(); err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | type Kubernetes struct { 69 | Kubeconfig string 70 | Client *kubernetes.Clientset 71 | IngressClass string 72 | } 73 | 74 | // Metrics contains the port & path for the 75 | // prometheus endpoint 76 | type Metrics struct { 77 | Host string 78 | Port int 79 | Path string 80 | } 81 | 82 | type Bind struct { 83 | Host string 84 | Port int 85 | } 86 | 87 | type Server struct { 88 | Default bool 89 | Regexp bool 90 | Host string 91 | Names []string 92 | Port int 93 | } 94 | 95 | // Valid returns an error if the metrics config is invalid 96 | func (m Metrics) Valid() error { 97 | if m.Port > maxTCPPort { 98 | return fmt.Errorf("Configured metrics port is above %d: port=%d", maxTCPPort, m.Port) 99 | } 100 | 101 | return nil 102 | } 103 | 104 | func LoadConfig(path string) (*Config, error) { 105 | glog.V(5).Infof("Loading config from: %s", path) 106 | 107 | fd, err := os.Open(path) 108 | if err != nil { 109 | return nil, err 110 | } 111 | config := Config{} 112 | err = json.NewDecoder(fd).Decode(&config) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | if err = config.Valid(); err != nil { 118 | return nil, err 119 | } 120 | 121 | config.setDefaultsIfUnset() 122 | 123 | glog.V(5).Infof("Read config: %v", config) 124 | 125 | return &config, err 126 | } 127 | 128 | func (c *Config) setDefaultsIfUnset() { 129 | if c.Bind.Port == 0 { 130 | c.Bind.Port = 8443 131 | } 132 | 133 | if c.Bind.Host == "" { 134 | glog.V(5).Infof("Bind host not set. Using default: 0.0.0.0") 135 | c.Bind.Host = "0.0.0.0" 136 | } 137 | 138 | if c.Metrics.Host == "" { 139 | glog.V(5).Infof("Metrics host not set. Using default: 0.0.0.0") 140 | c.Metrics.Host = c.Bind.Host 141 | } 142 | 143 | if c.Metrics.Port == 0 { 144 | glog.V(5).Infof("Metrics port not set. Using default: 9091") 145 | c.Metrics.Port = 9091 146 | } 147 | 148 | if c.Metrics.Path == "" { 149 | glog.V(5).Infof("Metrics path not set. Using default: /metrics") 150 | c.Metrics.Path = "/metrics" 151 | } 152 | if !strings.HasPrefix(c.Metrics.Path, "/") { 153 | c.Metrics.Path = "/" + c.Metrics.Path 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /k8sniff.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "bind": { 3 | "host": "localhost", 4 | "port": 8443 5 | }, 6 | "kubernetes": {}, 7 | "metrics": { 8 | "path": "/metrics", 9 | "port": 8080 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /kubernetes/example/bar.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: bar 5 | spec: 6 | ports: 7 | - port: 443 8 | selector: 9 | app: bar 10 | --- 11 | kind: Pod 12 | apiVersion: v1 13 | metadata: 14 | name: bar 15 | labels: 16 | app: bar 17 | spec: 18 | containers: 19 | - name: porter 20 | image: gcr.io/google_containers/porter:cd5cb5791ebaa8641955f0e8c2a9bed669b1eaab 21 | env: 22 | - name: SERVE_TLS_PORT_443 23 | value: bar 24 | - name: CERT_FILE 25 | value: "/localhost.crt" 26 | - name: KEY_FILE 27 | value: "/localhost.key" 28 | ports: 29 | - name: https 30 | containerPort: 443 31 | -------------------------------------------------------------------------------- /kubernetes/example/foo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: foo 5 | spec: 6 | ports: 7 | - port: 443 8 | selector: 9 | app: foo 10 | --- 11 | kind: Pod 12 | apiVersion: v1 13 | metadata: 14 | name: foo 15 | labels: 16 | app: foo 17 | spec: 18 | containers: 19 | - name: porter 20 | image: gcr.io/google_containers/porter:cd5cb5791ebaa8641955f0e8c2a9bed669b1eaab 21 | env: 22 | - name: SERVE_TLS_PORT_443 23 | value: foo 24 | - name: CERT_FILE 25 | value: "/localhost.crt" 26 | - name: KEY_FILE 27 | value: "/localhost.key" 28 | ports: 29 | - name: https 30 | containerPort: 443 31 | -------------------------------------------------------------------------------- /kubernetes/example/k8sniff.json: -------------------------------------------------------------------------------- 1 | { 2 | "bind": { 3 | "host": "localhost", 4 | "port": 8443 5 | }, 6 | "kubernetes": {} 7 | } 8 | -------------------------------------------------------------------------------- /kubernetes/example/tcp-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: tcp 5 | annotations: 6 | kubernetes.io/ingress.class: k8sniff 7 | spec: 8 | backend: 9 | serviceName: bar 10 | servicePort: 443 11 | rules: 12 | - host: foo 13 | http: 14 | paths: 15 | - backend: 16 | serviceName: foo 17 | servicePort: 443 18 | - host: bar 19 | http: 20 | paths: 21 | - backend: 22 | serviceName: bar 23 | servicePort: 443 24 | -------------------------------------------------------------------------------- /kubernetes/k8sniff-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: k8sniff-configmap 5 | namespace: k8sniff 6 | data: 7 | k8sniff.json: | 8 | { 9 | "bind": { 10 | "host": "0.0.0.0", 11 | "port": 8443 12 | }, 13 | "metrics": { 14 | "host": "0.0.0.0", 15 | "port": 9091, 16 | "path": "/metrics" 17 | }, 18 | "kubernetes": {} 19 | } 20 | -------------------------------------------------------------------------------- /kubernetes/k8sniff-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: k8sniff-ingress-lb 5 | namespace: k8sniff 6 | labels: 7 | role: k8sniff-ingress-lb 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | role: k8sniff-ingress-lb 13 | template: 14 | metadata: 15 | labels: 16 | role: k8sniff-ingress-lb 17 | spec: 18 | containers: 19 | - image: kubermatic/k8sniff:latest 20 | name: k8sniff-ingress-lb 21 | imagePullPolicy: Always 22 | command: 23 | - /bin/sh 24 | - -c 25 | - -x 26 | - "/pipeline/source/k8sniff -logtostderr --v=9 --config /etc/config/k8sniff.json" 27 | ports: 28 | - name: https 29 | containerPort: 8443 30 | volumeMounts: 31 | - name: k8sniff-config 32 | mountPath: /etc/config 33 | readOnly: true 34 | volumes: 35 | - name: k8sniff-config 36 | configMap: 37 | name: k8sniff-configmap 38 | -------------------------------------------------------------------------------- /kubernetes/k8sniff-namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: k8sniff 5 | -------------------------------------------------------------------------------- /kubernetes/k8sniff-svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: k8sniff-ingress-lb 5 | namespace: k8sniff 6 | spec: 7 | type: LoadBalancer 8 | ports: 9 | - port: 443 10 | targetPort: 8443 11 | protocol: TCP 12 | selector: 13 | role: k8sniff-ingress-lb 14 | -------------------------------------------------------------------------------- /logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kubermatic/k8sniff/e7435d989925e8559b0e5ca26da69f84a1035c32/logo/logo.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* {{{ Copyright (c) Paul R. Tagliamonte , 2015 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. }}} */ 20 | 21 | package main 22 | 23 | import ( 24 | "flag" 25 | "fmt" 26 | 27 | "github.com/kubermatic/k8sniff/metrics" 28 | ) 29 | 30 | func main() { 31 | var k8sniffConfig, kubeconfig string 32 | 33 | flag.StringVar(&k8sniffConfig, "config", "k8sniff.json", "Config") 34 | flag.StringVar(&kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file") 35 | flag.Parse() 36 | 37 | config, err := LoadConfig(k8sniffConfig) 38 | if err != nil { 39 | panic(err) 40 | } 41 | config.Kubernetes.Kubeconfig = kubeconfig 42 | 43 | go metrics.Serve(fmt.Sprintf("%s:%d", config.Metrics.Host, config.Metrics.Port), config.Metrics.Path) 44 | 45 | stop := make(chan struct{}) 46 | defer close(stop) 47 | go panic(config.Serve(stop)) 48 | <-stop 49 | } 50 | -------------------------------------------------------------------------------- /metrics/backends.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | var ( 8 | backendGauge = prometheus.NewGauge( 9 | prometheus.GaugeOpts{ 10 | Name: Prefix + "configured_backends_count", 11 | Help: "Number of configured backends", 12 | }, 13 | ) 14 | ) 15 | 16 | func SetBackendCount(count int) { 17 | backendGauge.Set(float64(count)) 18 | } 19 | -------------------------------------------------------------------------------- /metrics/connections.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | var ( 10 | connDurationsHisto = prometheus.NewHistogram(prometheus.HistogramOpts{ 11 | Name: Prefix + "connection_durations_histogram_seconds", 12 | Help: "Connection duration distributions.", 13 | }) 14 | connGauge = prometheus.NewGauge( 15 | prometheus.GaugeOpts{ 16 | Name: Prefix + "opened_connections_count", 17 | Help: "Number of opened TCP connections", 18 | }, 19 | ) 20 | ) 21 | 22 | // IncConnections increments the total connections counter 23 | func IncConnections() { 24 | connGauge.Inc() 25 | } 26 | 27 | func DecConnections() { 28 | connGauge.Dec() 29 | } 30 | 31 | // ConnectionTime gather the duration of a connection 32 | func ConnectionTime(d time.Duration) { 33 | connDurationsHisto.Observe(d.Seconds()) 34 | } 35 | -------------------------------------------------------------------------------- /metrics/doc.go: -------------------------------------------------------------------------------- 1 | // Package metrics contains some metrics that could be 2 | // collected by Prometheus. 3 | package metrics 4 | -------------------------------------------------------------------------------- /metrics/errors.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | ) 6 | 7 | const ( 8 | Error = "error" 9 | Fatal = "fatal" 10 | Info = "info" 11 | ) 12 | 13 | var ( 14 | errorCounterVec = prometheus.NewCounterVec( 15 | prometheus.CounterOpts{ 16 | Name: Prefix + "errors_total", 17 | Help: "Total error count", 18 | }, 19 | []string{"type"}, 20 | ) 21 | ) 22 | 23 | // IncErrors increments the total errors counter 24 | func IncErrors(typ string) { 25 | errorCounterVec.WithLabelValues(typ).Inc() 26 | } 27 | -------------------------------------------------------------------------------- /metrics/init.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | const ( 6 | Prefix = "k8sniff_" 7 | ) 8 | 9 | func init() { 10 | prometheus.MustRegister(connDurationsHisto) 11 | prometheus.MustRegister(connGauge) 12 | prometheus.MustRegister(errorCounterVec) 13 | prometheus.MustRegister(backendGauge) 14 | } 15 | -------------------------------------------------------------------------------- /metrics/serve.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/golang/glog" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | ) 10 | 11 | // Serve the prometheus metrics endpoint 12 | func Serve(addr, path string) { 13 | glog.V(1).Infof("Metrics exposed on %s%s", addr, path) 14 | 15 | http.Handle(path, promhttp.Handler()) 16 | log.Fatal(http.ListenAndServe(addr, nil)) 17 | } 18 | -------------------------------------------------------------------------------- /parser/parser.go: -------------------------------------------------------------------------------- 1 | /* {{{ Copyright (c) Paul R. Tagliamonte , 2015 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. }}} */ 20 | 21 | package parser 22 | 23 | import ( 24 | "fmt" 25 | ) 26 | 27 | var TLSHeaderLength = 5 28 | 29 | /* This function is basically all most folks want to invoke out of this 30 | * jumble of bits. This will take an incoming TLS Client Hello (including 31 | * all the fuzzy bits at the beginning of it - fresh out of the socket) and 32 | * go ahead and give us the SNI Name they want. */ 33 | func GetHostname(data []byte) (string, error) { 34 | if len(data) == 0 || data[0] != 0x16 { 35 | return "", fmt.Errorf("Doesn't look like a TLS Client Hello") 36 | } 37 | 38 | extensions, err := GetExtensionBlock(data) 39 | if err != nil { 40 | return "", err 41 | } 42 | sn, err := GetSNBlock(extensions) 43 | if err != nil { 44 | return "", err 45 | } 46 | sni, err := GetSNIBlock(sn) 47 | if err != nil { 48 | return "", err 49 | } 50 | return string(sni), nil 51 | } 52 | 53 | /* Given a Server Name TLS Extension block, parse out and return the SNI 54 | * (Server Name Indication) payload */ 55 | func GetSNIBlock(data []byte) ([]byte, error) { 56 | index := 0 57 | 58 | for { 59 | if index >= len(data) { 60 | break 61 | } 62 | length := int((data[index] << 8) + data[index+1]) 63 | endIndex := index + 2 + length 64 | if data[index+2] == 0x00 { /* SNI */ 65 | sni := data[index+3:] 66 | sniLength := int((sni[0] << 8) + sni[1]) 67 | return sni[2 : sniLength+2], nil 68 | } 69 | index = endIndex 70 | } 71 | return []byte{}, fmt.Errorf( 72 | "Finished parsing the SN block without finding an SNI", 73 | ) 74 | } 75 | 76 | /* Given a TLS Extensions data block, go ahead and find the SN block */ 77 | func GetSNBlock(data []byte) ([]byte, error) { 78 | index := 0 79 | 80 | if len(data) < 2 { 81 | return []byte{}, fmt.Errorf("Not enough bytes to be an SN block") 82 | } 83 | 84 | extensionLength := int((data[index] << 8) + data[index+1]) 85 | data = data[2 : extensionLength+2] 86 | 87 | for { 88 | if index >= len(data) { 89 | break 90 | } 91 | length := int((data[index+2] << 8) + data[index+3]) 92 | endIndex := index + 4 + length 93 | if data[index] == 0x00 && data[index+1] == 0x00 { 94 | return data[index+4 : endIndex], nil 95 | } 96 | 97 | index = endIndex 98 | } 99 | 100 | return []byte{}, fmt.Errorf( 101 | "Finished parsing the Extension block without finding an SN block", 102 | ) 103 | } 104 | 105 | /* Given a raw TLS Client Hello, go ahead and find all the Extensions */ 106 | func GetExtensionBlock(data []byte) ([]byte, error) { 107 | /* data[0] - content type 108 | * data[1], data[2] - major/minor version 109 | * data[3], data[4] - total length 110 | * data[...38+5] - start of SessionID (length bit) 111 | * data[38+5] - length of SessionID 112 | */ 113 | var index = TLSHeaderLength + 38 114 | 115 | if len(data) <= index+1 { 116 | return []byte{}, fmt.Errorf("Not enough bits to be a Client Hello") 117 | } 118 | 119 | /* Index is at SessionID Length bit */ 120 | if newIndex := index + 1 + int(data[index]); (newIndex + 2) < len(data) { 121 | index = newIndex 122 | } else { 123 | return []byte{}, fmt.Errorf("Not enough bytes for the SessionID") 124 | } 125 | 126 | /* Index is at Cipher List Length bits */ 127 | if newIndex := (index + 2 + int((data[index]<<8)+data[index+1])); (newIndex + 1) < len(data) { 128 | index = newIndex 129 | } else { 130 | return []byte{}, fmt.Errorf("Not enough bytes for the Cipher List") 131 | } 132 | 133 | /* Index is now at the compression length bit */ 134 | if newIndex := index + 1 + int(data[index]); newIndex < len(data) { 135 | index = newIndex 136 | } else { 137 | return []byte{}, fmt.Errorf("Not enough bytes for the compression length") 138 | } 139 | 140 | /* Now we're at the Extension start */ 141 | if len(data[index:]) == 0 { 142 | return nil, fmt.Errorf("No extensions") 143 | } 144 | return data[index:], nil 145 | } 146 | 147 | // vim: foldmethod=marker 148 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | /* {{{ Copyright (c) Paul R. Tagliamonte , 2015 2 | * 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. }}} */ 20 | 21 | package main 22 | 23 | import ( 24 | "fmt" 25 | "io" 26 | "math/rand" 27 | "net" 28 | "reflect" 29 | "regexp" 30 | "strings" 31 | "sync" 32 | "time" 33 | 34 | "github.com/golang/glog" 35 | "github.com/kubermatic/k8sniff/metrics" 36 | "github.com/kubermatic/k8sniff/parser" 37 | 38 | "k8s.io/api/core/v1" 39 | "k8s.io/api/extensions/v1beta1" 40 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 | "k8s.io/apimachinery/pkg/runtime" 42 | "k8s.io/apimachinery/pkg/util/intstr" 43 | "k8s.io/apimachinery/pkg/util/wait" 44 | "k8s.io/apimachinery/pkg/watch" 45 | "k8s.io/client-go/kubernetes" 46 | "k8s.io/client-go/tools/cache" 47 | "k8s.io/client-go/tools/clientcmd" 48 | ) 49 | 50 | const ( 51 | // ingressClassKey picks a specific "class" for the Ingress. The controller 52 | // only processes Ingresses with this annotation either unset, or set 53 | // to either nginxIngressClass or the empty string. 54 | ingressClassKey = "kubernetes.io/ingress.class" 55 | 56 | ConnectionClosedErr = "use of closed network connection" 57 | ConnectionResetErr = "connection reset by peer" 58 | ) 59 | 60 | // now provides func() time.Time 61 | // so it is easier to mock, if wou want to add tests 62 | var now = time.Now 63 | 64 | type ServerAndRegexp struct { 65 | Server *Server 66 | Regexp *regexp.Regexp 67 | } 68 | 69 | type Proxy struct { 70 | Lock sync.RWMutex 71 | ServerList []ServerAndRegexp 72 | Default *Server 73 | } 74 | 75 | func (p *Proxy) Get(host string) *Server { 76 | p.Lock.RLock() 77 | defer p.Lock.RUnlock() 78 | 79 | for _, tuple := range p.ServerList { 80 | if tuple.Regexp.MatchString(host) { 81 | return tuple.Server 82 | } 83 | } 84 | return p.Default 85 | } 86 | 87 | func (p *Proxy) Update(c *Config) error { 88 | servers := []ServerAndRegexp{} 89 | currentServers := c.Servers 90 | for i, server := range currentServers { 91 | for _, hostname := range server.Names { 92 | var hostRegexp *regexp.Regexp 93 | var err error 94 | if server.Regexp { 95 | hostRegexp, err = regexp.Compile(hostname) 96 | } else { 97 | hostRegexp, err = regexp.Compile("^" + regexp.QuoteMeta(hostname) + "$") 98 | } 99 | if err != nil { 100 | return fmt.Errorf("cannot update proxy due to invalid regex: %v", err) 101 | } 102 | tuple := ServerAndRegexp{¤tServers[i], hostRegexp} 103 | servers = append(servers, tuple) 104 | } 105 | } 106 | var def *Server 107 | for i, server := range currentServers { 108 | if server.Default { 109 | def = ¤tServers[i] 110 | break 111 | } 112 | } 113 | 114 | p.Lock.Lock() 115 | defer p.Lock.Unlock() 116 | p.ServerList = servers 117 | p.Default = def 118 | 119 | return nil 120 | } 121 | 122 | func (c *Config) UpdateServers() error { 123 | class := c.Kubernetes.IngressClass 124 | if class == "" { 125 | class = "k8sniff" 126 | } 127 | 128 | serverForBackend := func(ing *v1beta1.Ingress, backend *v1beta1.IngressBackend) (*Server, error) { 129 | obj, found, err := c.serviceStore.GetByKey(fmt.Sprintf("%s/%s", ing.Namespace, backend.ServiceName)) 130 | if err != nil { 131 | return nil, err 132 | } 133 | if !found { 134 | return nil, fmt.Errorf("service %s/%s not found", ing.Namespace, backend.ServiceName) 135 | } 136 | svc := obj.(*v1.Service) 137 | var port int 138 | if backend.ServicePort.Type == intstr.String { 139 | for _, p := range svc.Spec.Ports { 140 | if p.Name == backend.ServicePort.StrVal { 141 | port = int(p.Port) 142 | break 143 | } 144 | } 145 | if port == 0 { 146 | return nil, fmt.Errorf("port %s of service %s/%s not found", backend.ServicePort.StrVal, svc.Namespace, svc.Name) 147 | } 148 | } else { 149 | port = int(backend.ServicePort.IntVal) 150 | } 151 | return &Server{ 152 | Host: svc.Spec.ClusterIP, 153 | Port: port, 154 | }, nil 155 | } 156 | 157 | servers := []Server{} 158 | ingressList := c.ingressStore.List() 159 | for _, i := range ingressList { 160 | i := i.(*v1beta1.Ingress) 161 | name := fmt.Sprintf("%s/%s", i.Namespace, i.Name) 162 | if i.Annotations[ingressClassKey] != class { 163 | glog.V(6).Infof("Skipping ingress %s due to missing annotation. Expected %s=%s Got %s=%s", name, ingressClassKey, class, ingressClassKey, i.Annotations[ingressClassKey]) 164 | continue 165 | } 166 | 167 | if i.Spec.Backend != nil { 168 | s, err := serverForBackend(i, i.Spec.Backend) 169 | if err != nil { 170 | metrics.IncErrors(metrics.Error) 171 | glog.V(0).Infof("Ingress %s error with default backend, skipping: %v", name, err) 172 | } else { 173 | s.Default = true 174 | servers = append(servers, *s) 175 | } 176 | } 177 | for _, r := range i.Spec.Rules { 178 | if r.HTTP == nil { 179 | metrics.IncErrors(metrics.Error) 180 | glog.V(0).Infof("Ingress %s error with rule, skipping: http must be set", name) 181 | continue 182 | } 183 | for _, p := range r.HTTP.Paths { 184 | if p.Path != "" && p.Path != "/" { 185 | metrics.IncErrors(metrics.Error) 186 | glog.V(0).Infof("Ingress %s error with rule, skipping: path is not empty", name) 187 | continue 188 | } 189 | s, err := serverForBackend(i, &p.Backend) 190 | if err != nil { 191 | metrics.IncErrors(metrics.Error) 192 | glog.V(0).Infof("Ingress %s error with rule %q path %q, skipping: %v", name, r.Host, p.Path, err) 193 | continue 194 | } 195 | s.Names = []string{r.Host} 196 | glog.V(6).Infof("Adding backend %q -> %s:%d", r.Host, s.Host, s.Port) 197 | servers = append(servers, *s) 198 | } 199 | } 200 | } 201 | c.lock.Lock() 202 | defer c.lock.Unlock() 203 | if !reflect.DeepEqual(c.Servers, servers) { 204 | c.Servers = servers 205 | glog.V(2).Infof("Updating proxy configuration") 206 | err := c.proxy.Update(c) 207 | if err != nil { 208 | time.Sleep(time.Second) 209 | return fmt.Errorf("failed to update proxy: %v", err) 210 | } 211 | glog.V(2).Infof("================================================") 212 | glog.V(2).Infof("Updated servers. New servers:") 213 | c.PrintCurrentServers(2) 214 | glog.V(2).Infof("================================================") 215 | } 216 | 217 | metrics.SetBackendCount(len(c.Servers) - 1) 218 | 219 | return nil 220 | } 221 | 222 | func (c *Config) PrintCurrentServers(logLevel glog.Level) { 223 | for _, s := range c.Servers { 224 | hostnames := strings.Join(s.Names, ",") 225 | if hostnames == "" { 226 | hostnames = "default backend" 227 | } 228 | glog.V(logLevel).Infof("%s -> %s", hostnames, s.Host) 229 | } 230 | } 231 | 232 | func (c *Config) Debug() { 233 | glog.V(4).Info("================================================") 234 | glog.V(4).Info("Current configured servers:") 235 | c.PrintCurrentServers(4) 236 | glog.V(4).Info("================================================") 237 | } 238 | 239 | func (c *Config) TriggerUpdate() { 240 | if !c.ControllersHaveSynced() { 241 | return 242 | } 243 | err := c.UpdateServers() 244 | if err != nil { 245 | metrics.IncErrors(metrics.Info) 246 | glog.V(0).Infof("failed to update servers list: %v", err) 247 | } 248 | } 249 | 250 | func (c *Config) ControllersHaveSynced() bool { 251 | return c.ingressController.HasSynced() && c.serviceController.HasSynced() 252 | } 253 | 254 | func (c *Config) Serve(stopCh chan struct{}) error { 255 | glog.V(0).Infof("Listening on %s:%d", c.Bind.Host, c.Bind.Port) 256 | listener, err := net.Listen("tcp", fmt.Sprintf( 257 | "%s:%d", c.Bind.Host, c.Bind.Port, 258 | )) 259 | if err != nil { 260 | metrics.IncErrors(metrics.Fatal) 261 | return err 262 | } 263 | 264 | c.proxy = &Proxy{} 265 | err = c.proxy.Update(c) 266 | if err != nil { 267 | metrics.IncErrors(metrics.Fatal) 268 | return err 269 | } 270 | 271 | if c.Kubernetes != nil { 272 | cfg, err := clientcmd.BuildConfigFromFlags("", c.Kubernetes.Kubeconfig) 273 | if err != nil { 274 | panic(err) 275 | } 276 | c.Kubernetes.Client = kubernetes.NewForConfigOrDie(cfg) 277 | c.ingressStore, c.ingressController = cache.NewInformer( 278 | &cache.ListWatch{ 279 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 280 | return c.Kubernetes.Client.ExtensionsV1beta1().Ingresses("").List(options) 281 | }, 282 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 283 | return c.Kubernetes.Client.ExtensionsV1beta1().Ingresses("").Watch(options) 284 | }, 285 | }, 286 | &v1beta1.Ingress{}, 287 | 30*time.Minute, 288 | cache.ResourceEventHandlerFuncs{ 289 | AddFunc: func(obj interface{}) { 290 | go c.TriggerUpdate() 291 | }, 292 | UpdateFunc: func(old, cur interface{}) { 293 | go c.TriggerUpdate() 294 | }, 295 | DeleteFunc: func(obj interface{}) { 296 | go c.TriggerUpdate() 297 | }, 298 | }, 299 | ) 300 | 301 | c.serviceStore, c.serviceController = cache.NewInformer( 302 | &cache.ListWatch{ 303 | ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { 304 | return c.Kubernetes.Client.Services("").List(options) 305 | }, 306 | WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { 307 | return c.Kubernetes.Client.Services("").Watch(options) 308 | }, 309 | }, 310 | &v1.Service{}, 311 | 30*time.Minute, 312 | cache.ResourceEventHandlerFuncs{ 313 | AddFunc: func(obj interface{}) { 314 | go c.TriggerUpdate() 315 | }, 316 | UpdateFunc: func(old, cur interface{}) { 317 | go c.TriggerUpdate() 318 | }, 319 | DeleteFunc: func(obj interface{}) { 320 | go c.TriggerUpdate() 321 | }, 322 | }, 323 | ) 324 | 325 | go c.serviceController.Run(stopCh) 326 | go c.ingressController.Run(stopCh) 327 | } 328 | c.TriggerUpdate() 329 | 330 | go wait.Forever(func() { 331 | c.Debug() 332 | }, 30*time.Second) 333 | 334 | for { 335 | conn, err := listener.Accept() 336 | if err != nil { 337 | metrics.IncErrors(metrics.Error) 338 | return err 339 | } 340 | 341 | connectionID := RandomString(8) 342 | glog.V(4).Infof( 343 | "[%s] Proxy: %s -> %s", 344 | connectionID, 345 | conn.RemoteAddr(), 346 | conn.LocalAddr(), 347 | ) 348 | go c.proxy.Handle(conn, connectionID) 349 | } 350 | } 351 | 352 | func (p *Proxy) Handle(conn net.Conn, connectionID string) { 353 | metrics.IncConnections() 354 | start := now() 355 | defer func(s time.Time) { 356 | err := conn.Close() 357 | if err != nil { 358 | glog.V(0).Infof("[%s] Failed closing connection: %v", connectionID, err) 359 | metrics.IncErrors(metrics.Error) 360 | } 361 | metrics.DecConnections() 362 | metrics.ConnectionTime(now().Sub(s)) 363 | }(start) 364 | data := make([]byte, 4096) 365 | 366 | length, err := conn.Read(data) 367 | if err != nil { 368 | metrics.IncErrors(metrics.Error) 369 | glog.V(4).Infof("[%s] Error reading the first 4k of the connection: %v", connectionID, err) 370 | return 371 | } 372 | 373 | var proxy *Server 374 | hostname, hostnameErr := parser.GetHostname(data[:]) 375 | if hostnameErr == nil { 376 | glog.V(6).Infof("[%s] Parsed hostname: %s", connectionID, hostname) 377 | 378 | proxy = p.Get(hostname) 379 | if proxy == nil { 380 | glog.V(4).Infof("[%s] No proxy matched %s", connectionID, hostname) 381 | return 382 | } else { 383 | glog.V(4).Infof("[%s] Host found %s", connectionID, proxy.Host) 384 | } 385 | } else { 386 | glog.V(6).Info("[%s] Parsed request without hostname", connectionID) 387 | 388 | proxy = p.Default 389 | if proxy == nil { 390 | glog.V(4).Info("[%s] No default proxy", connectionID) 391 | return 392 | } 393 | } 394 | 395 | clientConn, err := net.Dial("tcp", fmt.Sprintf( 396 | "%s:%d", proxy.Host, proxy.Port, 397 | )) 398 | if err != nil { 399 | metrics.IncErrors(metrics.Error) 400 | glog.V(0).Infof("[%s] Error connecting to backend: %v", connectionID, err) 401 | return 402 | } 403 | 404 | defer func() { 405 | err := clientConn.Close() 406 | if err != nil { 407 | glog.V(0).Infof("[%s] Failed closing client connection: %v", connectionID, err) 408 | metrics.IncErrors(metrics.Error) 409 | } 410 | }() 411 | 412 | n, err := clientConn.Write(data[:length]) 413 | glog.V(7).Infof("[%s] Wrote %d bytes", connectionID, n) 414 | if err != nil { 415 | metrics.IncErrors(metrics.Info) 416 | glog.V(7).Infof("[%s] Error sending data to backend: %v", connectionID, err) 417 | return 418 | } 419 | Copycat(clientConn, conn, connectionID) 420 | } 421 | 422 | func Copycat(client, server net.Conn, connectionID string) { 423 | glog.V(6).Infof("[%s] Initiating copy between %s and %s", connectionID, client.RemoteAddr().String(), server.RemoteAddr().String()) 424 | 425 | doCopy := func(s, c net.Conn, cancel chan<- bool) { 426 | glog.V(7).Infof("[%s] Established connection %s -> %s", connectionID, s.RemoteAddr().String(), c.RemoteAddr().String()) 427 | _, err := io.Copy(s, c) 428 | if err != nil && !strings.Contains(err.Error(), ConnectionClosedErr) && !strings.Contains(err.Error(), ConnectionResetErr) { 429 | glog.V(0).Infof("[%s] Failed copying connection data: %v", connectionID, err) 430 | metrics.IncErrors(metrics.Error) 431 | } 432 | glog.V(7).Infof("[%s] Destroyed connection %s -> %s", connectionID, s.RemoteAddr().String(), c.RemoteAddr().String()) 433 | cancel <- true 434 | } 435 | 436 | cancel := make(chan bool, 2) 437 | 438 | go doCopy(server, client, cancel) 439 | go doCopy(client, server, cancel) 440 | 441 | select { 442 | case <-cancel: 443 | glog.V(6).Infof("[%s] Disconnected", connectionID) 444 | return 445 | } 446 | } 447 | 448 | func RandomString(strlen int) string { 449 | rand.Seed(time.Now().UTC().UnixNano()) 450 | const chars = "abcdefghijklmnopqrstuvwxyz0123456789" 451 | result := make([]byte, strlen) 452 | for i := 0; i < strlen; i++ { 453 | result[i] = chars[rand.Intn(len(chars))] 454 | } 455 | return string(result) 456 | } 457 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | # The container definition we want to use for developing our app 2 | box: golang:1.8 3 | build: 4 | steps: 5 | - setup-go-workspace: 6 | package-dir: github.com/kubermatic/k8sniff 7 | - script: 8 | name: install go tools 9 | code: | 10 | go version 11 | go get -u -v github.com/golang/dep 12 | go get -u -v github.com/golang/dep/cmd/dep 13 | - script: 14 | name: install dependencies 15 | code: | 16 | dep ensure 17 | # Statically build the project 18 | - script: 19 | name: go build 20 | code: CGO_ENABLED=0 go build -a -ldflags '-s' -installsuffix cgo -o k8sniff . 21 | # Test the project 22 | - script: 23 | name: go test 24 | code: go test $(glide nv) 25 | # Copy binary to a location that gets passed along to the deploy pipeline 26 | - script: 27 | name: copy to deployment directory 28 | code: | 29 | cp -av k8sniff "$WERCKER_OUTPUT_DIR" 30 | 31 | deploy: 32 | box: 33 | id: alpine:3.6 34 | cmd: /bin/sh 35 | docker-push: 36 | - internal/docker-push: 37 | name: push to docker hub as latest 38 | author: Sebastian Scheele 39 | cmd: /pipeline/source/k8sniff 40 | username: $USERNAME 41 | password: $PASSWORD 42 | email: scheele.s@web.de 43 | tag: $WERCKER_GIT_COMMIT 44 | repository: kubermatic/k8sniff 45 | registry: https://registry.hub.docker.com 46 | --------------------------------------------------------------------------------