diff --git a/hack/.linted_packages b/hack/.linted_packages index f85743ce17..c3c867a682 100644 --- a/hack/.linted_packages +++ b/hack/.linted_packages @@ -235,6 +235,8 @@ test/images/goproxy test/images/logs-generator test/images/mount-tester test/images/n-way-http +test/images/net +test/images/net/common test/images/port-forward-tester test/images/porter test/images/resource-consumer/consume-cpu diff --git a/test/images/net/.gitignore b/test/images/net/.gitignore new file mode 100644 index 0000000000..b0c8d5c3d7 --- /dev/null +++ b/test/images/net/.gitignore @@ -0,0 +1 @@ +/net diff --git a/test/images/net/BUILD b/test/images/net/BUILD new file mode 100644 index 0000000000..dcf630f9df --- /dev/null +++ b/test/images/net/BUILD @@ -0,0 +1,21 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_binary", + "go_library", + "go_test", + "cgo_library", +) + +go_binary( + name = "net", + srcs = ["main.go"], + tags = ["automanaged"], + deps = [ + "//test/images/net/common:go_default_library", + "//test/images/net/nat:go_default_library", + ], +) diff --git a/test/images/net/Dockerfile b/test/images/net/Dockerfile new file mode 100644 index 0000000000..e54f38343b --- /dev/null +++ b/test/images/net/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM alpine +MAINTAINER Bowei Du +COPY net /net +RUN apk update && apk add curl diff --git a/test/images/net/Makefile b/test/images/net/Makefile new file mode 100644 index 0000000000..8be371d8cc --- /dev/null +++ b/test/images/net/Makefile @@ -0,0 +1,39 @@ +# Copyright 2016 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +ARCH := amd64 +PREFIX ?= gcr.io/google_containers +TAG ?= 1.0 +IMAGE ?= e2e-net-$(ARCH) + +SRCS := $(shell find . -name \*.go) + +all: image + +net: $(SRCS) + CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-w' + +image: test net + docker build -t $(PREFIX)/$(IMAGE):$(TAG) . + +push: image + gcloud docker -- push $(PREFIX)/$(IMAGE):$(TAG) + +clean: + rm -f net + +test: + go test ./... + +.PHONY: all clean image push test diff --git a/test/images/net/README.md b/test/images/net/README.md new file mode 100644 index 0000000000..e81002a202 --- /dev/null +++ b/test/images/net/README.md @@ -0,0 +1,36 @@ +# Overview + +The goal of this Go project is to consolidate all low-level +network testing "daemons" into one place. In network testing we +frequently have need of simple daemons (common/Runner) that perform +some "trivial" set of actions on a socket. + +# Usage + +* A package for each general area that is being tested, for example + `nat/` will contain Runners that test various NAT features. +* Every runner should be registered via `main.go:makeRunnerMap()`. +* Runners receive a JSON options structure as to their configuration. `Run()` + should return the disposition of the test. + +Runners can be executed into two different ways, either through the +the command-line or via an HTTP request: + +## Command-line + +```` +$ ./net -runner -options +./net \ + -runner nat-closewait-client \ + -options '{"RemoteAddr":"127.0.0.1:9999"}' +```` + +## HTTP server +```` +$ ./net --serve :8889 +$ curl -v -X POST localhost:8889/run/nat-closewait-server \ + -d '{"LocalAddr":"127.0.0.1:9999"}' +```` + + +[![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/test/images/net/README.md?pixel)]() diff --git a/test/images/net/common/BUILD b/test/images/net/common/BUILD new file mode 100644 index 0000000000..e3bf654e1d --- /dev/null +++ b/test/images/net/common/BUILD @@ -0,0 +1,17 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_binary", + "go_library", + "go_test", + "cgo_library", +) + +go_library( + name = "go_default_library", + srcs = ["common.go"], + tags = ["automanaged"], +) diff --git a/test/images/net/common/common.go b/test/images/net/common/common.go new file mode 100644 index 0000000000..a2ee5cb787 --- /dev/null +++ b/test/images/net/common/common.go @@ -0,0 +1,29 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import "log" + +// Runner is a client or server to run. +type Runner interface { + // NewOptions returns a new empty options structure to be populated + // by from the JSON -options argument. + NewOptions() interface{} + // Run the client or server, taking in options. This execute the + // test code. + Run(logger *log.Logger, options interface{}) error +} diff --git a/test/images/net/main.go b/test/images/net/main.go new file mode 100644 index 0000000000..600e273f2d --- /dev/null +++ b/test/images/net/main.go @@ -0,0 +1,162 @@ +/* +Copyright 2014 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "strings" + + "k8s.io/kubernetes/test/images/net/common" + "k8s.io/kubernetes/test/images/net/nat" +) + +type runnerMap map[string]common.Runner + +type runRequestJSON struct { + runner string + options interface{} +} + +var ( + // flags for the command line. See usage args below for + // descriptions. + flags struct { + Serve string + Runner string + Options string + } + // runners is a map from runner name to runner instance. + runners = makeRunnerMap() +) + +type logOutput struct { + b bytes.Buffer +} + +func main() { + initFlags() + log.SetFlags(log.Flags() | log.Lshortfile) + + if flags.Serve == "" { + output, err := executeRunner(flags.Runner, flags.Options) + if err == nil { + fmt.Print("output:\n\n" + output.b.String()) + os.Exit(0) + } else { + log.Printf("Error: %v", err) + fmt.Print("output:\n\n" + output.b.String()) + os.Exit(1) + } + } else { + http.HandleFunc("/run/", handleRunRequest) + log.Printf("Running server on %v", flags.Serve) + log.Fatal(http.ListenAndServe(flags.Serve, nil)) + } +} + +func initFlags() { + legalRunners := "" + for k := range runners { + legalRunners += " " + k + } + flag.StringVar( + &flags.Serve, "serve", "", + "Address and port to bind to (e.g. 127.0.0.1:8080). Setting this will "+ + "run the network tester in server mode runner are triggered through "+ + "HTTP requests.") + flag.StringVar( + &flags.Runner, "runner", "", + "Runner to execute (available:"+legalRunners+")") + flag.StringVar( + &flags.Options, "options", "", + "JSON options to the Runner") + flag.Parse() + + if flags.Runner == "" && flags.Serve == "" { + log.Fatalf("Must set either -runner or -serve, see --help") + } +} + +func makeRunnerMap() runnerMap { + // runner name is --. + return runnerMap{ + "nat-closewait-client": nat.NewCloseWaitClient(), + "nat-closewait-server": nat.NewCloseWaitServer(), + } +} + +func executeRunner(name string, rawOptions string) (logOutput, error) { + runner, ok := runners[name] + if ok { + options := runner.NewOptions() + if err := json.Unmarshal([]byte(rawOptions), options); err != nil { + return logOutput{}, fmt.Errorf("Invalid options JSON: %v", err) + } + + log.Printf("Options: %+v", options) + + output := logOutput{} + logger := log.New(&output.b, "# ", log.Lshortfile) + + return output, runner.Run(logger, options) + } + + return logOutput{}, fmt.Errorf("Invalid runner: '%v', see --help\n", runner) +} + +// handleRunRequest handles a request JSON to the network tester. +func handleRunRequest(w http.ResponseWriter, r *http.Request) { + log.Printf("handleRunRequest %v", *r) + + urlParts := strings.Split(r.URL.Path, "/") + if len(urlParts) != 3 { + http.Error(w, fmt.Sprintf("invalid request to run: %v", urlParts), 400) + return + } + + runner := urlParts[2] + if r.Body == nil { + http.Error(w, "Missing request body", 400) + return + } + + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, fmt.Sprintf("error reading body: %v", err), 400) + return + } + + var output logOutput + if output, err = executeRunner(runner, string(body)); err != nil { + contents := fmt.Sprintf("Error from runner: %v\noutput:\n\n%s", + err, output.b.String()) + http.Error(w, contents, 500) + return + } + + fmt.Fprintf(w, "ok\noutput:\n\n"+output.b.String()) +} + +func setupLogger() { +} diff --git a/test/images/net/nat/BUILD b/test/images/net/nat/BUILD new file mode 100644 index 0000000000..ccb7f9180d --- /dev/null +++ b/test/images/net/nat/BUILD @@ -0,0 +1,18 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_binary", + "go_library", + "go_test", + "cgo_library", +) + +go_library( + name = "go_default_library", + srcs = ["closewait.go"], + tags = ["automanaged"], + deps = ["//test/images/net/common:go_default_library"], +) diff --git a/test/images/net/nat/closewait.go b/test/images/net/nat/closewait.go new file mode 100644 index 0000000000..b9f804cea0 --- /dev/null +++ b/test/images/net/nat/closewait.go @@ -0,0 +1,193 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nat + +/* +client/server for testing CLOSE_WAIT timeout condition in iptables NAT. + +client server + | | + |<--tcp handshake-->| + |<-------fin--------| half-close from server + | | client is in CLOSE_WAIT +*/ + +import ( + "errors" + "io" + "log" + "net" + "time" + + "k8s.io/kubernetes/test/images/net/common" +) + +// leakedConnection is a +var leakedConnection *net.TCPConn + +// Server JSON options. +type CloseWaitServerOptions struct { + // Address to bind for the test + LocalAddr string + // Timeout to wait after sending the FIN. + PostFinTimeoutSeconds int +} + +type closeWaitServer struct { + options *CloseWaitServerOptions +} + +// NewCloseWaitServer returns a new Runner. +func NewCloseWaitServer() common.Runner { + return &closeWaitServer{} +} + +// NewOptions allocates new options structure. +func (server *closeWaitServer) NewOptions() interface{} { + return &CloseWaitServerOptions{} +} + +// Run the server-side of the test. +func (server *closeWaitServer) Run(logger *log.Logger, rawOptions interface{}) error { + if options, ok := rawOptions.(*CloseWaitServerOptions); ok { + server.options = options + } else { + return errors.New("invalid type") + } + + logger.Printf("Run %v", server.options) + + addr, err := net.ResolveTCPAddr("tcp", server.options.LocalAddr) + if err != nil { + return err + } + + listener, err := net.ListenTCP("tcp", addr) + if err != nil { + return err + } + defer listener.Close() + + logger.Printf("Server listening on %v", addr) + + conn, err := listener.AcceptTCP() + if err != nil { + return err + } + defer conn.Close() + + logger.Printf("Client connected") + + // Send client half-close FIN so client is now in CLOSE_WAIT. We keep + // the client -> server pipe open to verify whether or not the NAT + // dropped our connection. + if err := conn.CloseWrite(); err != nil { + return err + } + + logger.Printf("Server sent FIN, waiting %v seconds", + server.options.PostFinTimeoutSeconds) + + <-time.After(time.Duration(server.options.PostFinTimeoutSeconds) * time.Second) + + logger.Printf("Done") + + return nil +} + +// Client JSON options +type CloseWaitClientOptions struct { + // RemoteAddr of the server to connect to. + RemoteAddr string + // TimeoutSeconds on I/O with the server. + TimeoutSeconds int + // Half-close timeout (to give the test time to check the status of the + // conntrack table entry. + PostFinTimeoutSeconds int + // Leak connection (assign to global variable so connection persists + // as long as the process remains. + LeakConnection bool +} + +type closeWaitClient struct { + options *CloseWaitClientOptions +} + +// NewCloseWaitClient creates a new runner +func NewCloseWaitClient() common.Runner { + return &closeWaitClient{} +} + +// NewOptions allocates new options structure. +func (client *closeWaitClient) NewOptions() interface{} { + return &CloseWaitClientOptions{} +} + +// Run the client.m +func (client *closeWaitClient) Run(logger *log.Logger, rawOptions interface{}) error { + if options, ok := rawOptions.(*CloseWaitClientOptions); ok { + client.options = options + } else { + return errors.New("invalid type") + } + + logger.Printf("Run %v", client.options) + + addr, err := net.ResolveTCPAddr("tcp", client.options.RemoteAddr) + if err != nil { + return err + } + + conn, err := net.DialTCP("tcp", nil, addr) + if err != nil { + return err + } + defer conn.Close() + + logger.Printf("Connected to server") + + if client.options.TimeoutSeconds > 0 { + delay := time.Duration(client.options.TimeoutSeconds) * time.Second + conn.SetReadDeadline(time.Now().Add(delay)) + } + + buf := make([]byte, 1, 1) + size, err := conn.Read(buf) + + if err != nil && err != io.EOF { + return err + } + + if size != 0 { + return errors.New("Got data but expected EOF") + } + + logger.Printf("Server has half-closed the connection, waiting %v seconds", + client.options.PostFinTimeoutSeconds) + + if client.options.LeakConnection { + logger.Printf("Leaking client connection (assigning to global variable)") + leakedConnection = conn + } + + <-time.After( + time.Duration(client.options.PostFinTimeoutSeconds) * time.Second) + + logger.Printf("Done") + + return nil +}