├── .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 |
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 |
--------------------------------------------------------------------------------