├── .adr-dir ├── .dockerignore ├── .gitattributes ├── .github ├── pull_request_template.md ├── renovate.json └── workflows │ ├── PR.yaml │ ├── label-issues.yml │ ├── merge.yaml │ ├── release.yaml │ └── renovate-vault.yml ├── .gitignore ├── .golangci.json ├── .goreleaser.yml ├── Dockerfile ├── LICENSE ├── README.md ├── cmd ├── client │ ├── app │ │ ├── cmd.go │ │ └── info.go │ ├── cmd.go │ ├── hns │ │ ├── cmd.go │ │ └── get_network.go │ ├── host │ │ ├── cmd.go │ │ └── get_version.go │ ├── internal │ │ └── grpc.go │ ├── network │ │ ├── cmd.go │ │ └── get.go │ ├── process │ │ ├── cmd.go │ │ └── run.go │ ├── proxy │ │ ├── cmd.go │ │ └── publish.go │ └── route │ │ ├── add.go │ │ └── cmd.go ├── cmds │ ├── flags │ │ ├── list_value.go │ │ └── list_value_test.go │ └── tools.go ├── grpcs │ ├── log.go │ └── process_path_whitelist.go ├── main.go ├── outputs │ └── response.go ├── server │ ├── app │ │ ├── cmd.go │ │ ├── run.go │ │ ├── run_grpc.go │ │ └── run_service.go │ ├── cmd.go │ └── config │ │ └── config.go └── stackdump │ ├── cmd.go │ └── stackdump.go ├── docs └── adrs │ ├── 0001-record-architecture-decisions.md │ ├── 0002-add-system-agent-support-to-wins.md │ ├── 0003-system-agent-configuration.md │ └── 0004-csi-proxy-and-configuration.md ├── go.mod ├── go.sum ├── install.ps1 ├── magefiles └── magefile.go ├── magetools ├── gotool.go └── helpers.go ├── manifest.tmpl ├── pkg ├── apis │ ├── api.go │ ├── application_service.go │ ├── hns_service.go │ ├── host_service.go │ ├── network_service.go │ ├── process_service.go │ ├── process_service_mgmt.go │ └── route_service.go ├── certs │ └── certs.go ├── concierge │ └── concierge.go ├── converters │ ├── binary.go │ ├── byte.go │ ├── byte_test.go │ ├── error_ignore.go │ ├── json.go │ └── json_test.go ├── csiproxy │ └── csi.go ├── defaults │ ├── constant.go │ └── variable.go ├── logs │ └── etw.go ├── npipes │ ├── npipe.go │ └── npipe_test.go ├── panics │ └── recover.go ├── paths │ ├── directory.go │ ├── file.go │ └── file_binary.go ├── powershell │ ├── powershell.go │ └── powershell_test.go ├── profilings │ ├── dump.go │ ├── profiling.go │ └── stack.go ├── proxy │ ├── client.go │ └── server.go ├── syscalls │ └── route.go ├── systemagent │ └── agent.go ├── tls │ └── tls.go └── types │ ├── application.pb.go │ ├── application.proto │ ├── common.pb.go │ ├── common.proto │ ├── hns.pb.go │ ├── hns.proto │ ├── host.pb.go │ ├── host.proto │ ├── network.pb.go │ ├── network.proto │ ├── process.pb.go │ ├── process.proto │ ├── route.pb.go │ └── route.proto ├── scripts ├── build-image.ps1 ├── integration.ps1 └── protoc-gen.sh ├── suc ├── main.go ├── pkg │ ├── cmd.go │ ├── host │ │ ├── common.go │ │ ├── common_test.go │ │ ├── embed.go │ │ └── upgrade.go │ ├── rancher │ │ └── connectionInfo.go │ ├── service │ │ ├── common.go │ │ ├── common_test.go │ │ ├── config │ │ │ ├── config.go │ │ │ └── config_test.go │ │ ├── configure.go │ │ ├── service.go │ │ ├── service_rancher_wins.go │ │ └── service_rke2.go │ └── state │ │ └── state.go └── update-connection-info.ps1 ├── tests └── integration │ ├── application_test.ps1 │ ├── docker │ ├── Dockerfile.wins-cli │ ├── Dockerfile.wins-nginx │ └── nginx.ps1 │ ├── hns.psm1 │ ├── hns_test.ps1 │ ├── host_test.ps1 │ ├── install_test.ps1 │ ├── integration_suite_test.ps1 │ ├── network_test.ps1 │ ├── process_test.ps1 │ ├── route_test.ps1 │ ├── suc_test.ps1 │ ├── suc_upgrade_test.ps1 │ └── utils.psm1 └── uninstall.ps1 /.adr-dir: -------------------------------------------------------------------------------- 1 | docs/adrs 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | 4 | # CMake 5 | *.exe~ 6 | *.exe 7 | cmake-build-debug/ 8 | cmake-build-release/ 9 | 10 | # Binaries for programs and plugins 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | # Output of the go coverage tool, specifically when used with ginkgo 22 | *.coverprofile 23 | 24 | # MAC file 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect 2 | ## Handle line endings automatically for files detected as 3 | ## text and leave all files detected as binary untouched. 4 | ## This will handle all files NOT defined below. 5 | * text=auto 6 | 7 | # Source code 8 | *.go -text diff=golang 9 | *.sum -text 10 | *.mod -text 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ### Summary 3 | Fixes # 4 | 5 | 6 | ### Occurred changes and/or fixed issues 7 | 8 | 9 | ### Technical notes summary 10 | 11 | 12 | ### Areas or cases that should be tested 13 | 14 | 15 | 16 | ### Areas which could experience regressions 17 | 18 | 19 | ### Screenshot/Video 20 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>rancher/renovate-config#release" 4 | ], 5 | "baseBranches": [ 6 | "main" 7 | ], 8 | "prHourlyLimit": 2, 9 | "reviewers": ["HarrisonWAffel"] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/PR.yaml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | 3 | on: 4 | pull_request: 5 | 6 | permissions: 7 | contents: read 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | platform: [windows-2019, windows-latest] 15 | runs-on: ${{ matrix.platform }} 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | # This step is required otherwise the 'mage' 22 | # command cannot be used in subsequent steps 23 | - name: Install Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: 'stable' 27 | 28 | - name: Install Dependencies 29 | run: | 30 | go install github.com/magefile/mage@v1.15.0 31 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7 32 | 33 | - name: Build 34 | shell: pwsh 35 | run: | 36 | set PSModulePath=&&powershell -command "mage BuildAll" 37 | 38 | - name: Run E2E tests 39 | shell: pwsh 40 | run: | 41 | Install-Module -Name DockerMsftProvider -Force 42 | Import-Module -Name HostNetworkingService 43 | set PSModulePath=&&powershell -command "mage TestAll" 44 | -------------------------------------------------------------------------------- /.github/workflows/label-issues.yml: -------------------------------------------------------------------------------- 1 | name: Label issues 2 | on: 3 | issues: 4 | types: 5 | - reopened 6 | - opened 7 | jobs: 8 | label_issues: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - run: gh issue edit "$NUMBER" --add-label "$LABELS" 14 | env: 15 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | GH_REPO: ${{ github.repository }} 17 | NUMBER: ${{ github.event.issue.number }} 18 | LABELS: "team/area2" -------------------------------------------------------------------------------- /.github/workflows/merge.yaml: -------------------------------------------------------------------------------- 1 | name: Merge 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | platform: [windows-2019, windows-latest] 17 | runs-on: ${{ matrix.platform }} 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v4 22 | 23 | # This step is required otherwise the 'mage' 24 | # command cannot be used in subsequent steps 25 | - name: Install Go 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: 'stable' 29 | 30 | - name: Install Dependencies 31 | run: | 32 | go install github.com/magefile/mage@v1.15.0 33 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.7 34 | 35 | - name: Build 36 | shell: pwsh 37 | run: | 38 | set PSModulePath=&&powershell -command "mage BuildAll" 39 | 40 | - name: Run E2E tests 41 | shell: pwsh 42 | run: | 43 | Install-Module -Name DockerMsftProvider -Force 44 | Import-Module -Name HostNetworkingService 45 | set PSModulePath=&&powershell -command "mage TestAll" 46 | -------------------------------------------------------------------------------- /.github/workflows/renovate-vault.yml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: "Override default log level" 7 | required: false 8 | default: "info" 9 | type: string 10 | overrideSchedule: 11 | description: "Override all schedules" 12 | required: false 13 | default: "false" 14 | type: string 15 | schedule: 16 | # Runs twice on weekdays. 17 | - cron: '30 4,6 * * 1-5' 18 | 19 | permissions: 20 | contents: read 21 | id-token: write 22 | 23 | jobs: 24 | call-workflow: 25 | uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@release 26 | with: 27 | logLevel: ${{ inputs.logLevel || 'info' }} 28 | overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }} 29 | secrets: inherit 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | .vscode/ 4 | 5 | # CMake 6 | cmake-build-debug/ 7 | cmake-build-release/ 8 | 9 | # Binaries for programs and plugins 10 | bin/ 11 | !.gitkeep 12 | *.exe 13 | *.exe~ 14 | *.dll 15 | *.so 16 | *.dylib 17 | *.test-* 18 | 19 | # Test binary, build with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Output of the go coverage tool, specifically when used with ginkgo 26 | *.coverprofile 27 | 28 | # Rancher 29 | Dockerfile.Dapper[0-9]* 30 | Dockerfile.dapper[0-9]* 31 | .cache/ 32 | 33 | # MAC file 34 | .DS_Store 35 | 36 | dist/ 37 | *.tgz 38 | 39 | artifacts/ 40 | -------------------------------------------------------------------------------- /.golangci.json: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "disable-all": true, 4 | "enable": [ 5 | "govet", 6 | "revive", 7 | "misspell", 8 | "ineffassign", 9 | "gofmt" 10 | ] 11 | }, 12 | "issues": { 13 | "exclude-files": [ 14 | "/zz_generated_", 15 | "docs", 16 | "tests", 17 | "scripts", 18 | "charts", 19 | "package", 20 | "pkg/powershell/powershell.go", 21 | "suc/pkg/host/embed.go" 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: wins 6 | env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - windows 10 | goarch: 11 | - amd64 12 | mod_timestamp: '{{ .CommitTimestamp }}' 13 | binary: wins 14 | main: cmd/main.go 15 | ldflags: 16 | - -s -w -X github.com/rancher/wins/pkg/defaults.AppVersion={{.Version}} -X github.com/rancher/wins/pkg/defaults.AppCommit={{.Commit}} -extldflags "-static" 17 | - id: wins-container 18 | env: 19 | - CGO_ENABLED=0 20 | goos: 21 | - windows 22 | goarch: 23 | - amd64 24 | mod_timestamp: '{{ .CommitTimestamp }}' 25 | binary: wins-container 26 | main: cmd/main.go 27 | ldflags: 28 | - -s -w -X github.com/rancher/wins/pkg/defaults.AppVersion={{.Version}} -X github.com/rancher/wins/pkg/defaults.AppCommit=container -extldflags "-static" 29 | 30 | checksum: 31 | name_template: 'sha256sum.txt' 32 | snapshot: 33 | name_template: "{{ incpatch .Version }}-next" 34 | changelog: 35 | sort: asc 36 | filters: 37 | exclude: 38 | - '^docs:' 39 | - '^test:' 40 | - '^tests:' 41 | - '^scripts:' 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NANOSERVER_VERSION 2 | FROM mcr.microsoft.com/windows/nanoserver:${NANOSERVER_VERSION} as wins 3 | ARG VERSION 4 | ARG MAINTAINERS 5 | ARG REPO 6 | 7 | SHELL ["powershell", "-NoLogo", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] 8 | 9 | ENV VERSION ${VERSION} 10 | ENV MAINTAINERS ${MAINTAINERS} 11 | ENV REPO ${REPO} 12 | 13 | LABEL org.opencontainers.image.authors=${MAINTAINERS} 14 | LABEL org.opencontainers.image.url=${REPO} 15 | LABEL org.opencontainers.image.documentation=${REPO} 16 | LABEL org.opencontainers.image.source=${REPO} 17 | LABEL org.label-schema.vcs-url=${REPO} 18 | LABEL org.opencontainers.image.vendor="Rancher Labs" 19 | LABEL org.opencontainers.image.version=${VERSION} 20 | WORKDIR C:/ 21 | 22 | COPY ./artifacts/wins-suc.exe C:/ 23 | COPY ./suc/update-connection-info.ps1 C:/ 24 | 25 | USER ContainerAdministrator 26 | ENTRYPOINT [ "wins-suc.exe" ] 27 | -------------------------------------------------------------------------------- /cmd/client/app/cmd.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/pkg/defaults" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func NewCommand() *cli.Command { 11 | return &cli.Command{ 12 | Name: "app", 13 | Aliases: []string{"application"}, 14 | Usage: fmt.Sprintf("Manage %s Application", defaults.WindowsServiceDisplayName), 15 | Subcommands: []*cli.Command{ 16 | infoCommand(), 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/client/app/info.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rancher/wins/cmd/client/internal" 9 | "github.com/rancher/wins/cmd/outputs" 10 | "github.com/rancher/wins/pkg/defaults" 11 | "github.com/rancher/wins/pkg/panics" 12 | "github.com/rancher/wins/pkg/paths" 13 | "github.com/rancher/wins/pkg/types" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | var _infoFlags = internal.NewGRPCClientConn([]cli.Flag{}) 18 | 19 | func _infoAction(cliCtx *cli.Context) (err error) { 20 | defer panics.Log() 21 | 22 | clientChecksum, err := paths.GetBinarySHA1Hash(os.Args[0]) 23 | if err != nil { 24 | return errors.Wrap(err, "failed to get checksum for execution binary") 25 | } 26 | 27 | ctx, cancel := context.WithCancel(context.Background()) 28 | defer cancel() 29 | 30 | // parse grpc client connection 31 | grpcClientConn, err := internal.ParseGRPCClientConn(cliCtx) 32 | if err != nil { 33 | return err 34 | } 35 | defer func() { 36 | closeErr := grpcClientConn.Close() 37 | if err == nil { 38 | err = closeErr 39 | } 40 | }() 41 | 42 | // start client 43 | client := types.NewApplicationServiceClient(grpcClientConn) 44 | infoResp, err := client.Info(ctx, &types.Void{}) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return outputs.JSON(cliCtx.App.Writer, map[string]interface{}{ 50 | "Client": &types.ApplicationInfo{Checksum: clientChecksum, Version: defaults.AppVersion, Commit: defaults.AppCommit}, 51 | "Server": infoResp.Info, 52 | }) 53 | } 54 | 55 | func infoCommand() *cli.Command { 56 | return &cli.Command{ 57 | Name: "info", 58 | Usage: "Get application info", 59 | Flags: _infoFlags, 60 | Action: _infoAction, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /cmd/client/cmd.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/cmd/client/app" 7 | "github.com/rancher/wins/cmd/client/hns" 8 | "github.com/rancher/wins/cmd/client/host" 9 | "github.com/rancher/wins/cmd/client/network" 10 | "github.com/rancher/wins/cmd/client/process" 11 | "github.com/rancher/wins/cmd/client/proxy" 12 | "github.com/rancher/wins/cmd/client/route" 13 | "github.com/rancher/wins/pkg/defaults" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | func NewCommand() *cli.Command { 18 | return &cli.Command{ 19 | Name: "cli", 20 | Aliases: []string{"client"}, 21 | Description: fmt.Sprintf("The client side commands of %s", defaults.WindowsServiceDisplayName), 22 | Subcommands: []*cli.Command{ 23 | hns.NewCommand(), 24 | host.NewCommand(), 25 | network.NewCommand(), 26 | process.NewCommand(), 27 | route.NewCommand(), 28 | app.NewCommand(), 29 | proxy.NewCommand(), 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /cmd/client/hns/cmd.go: -------------------------------------------------------------------------------- 1 | package hns 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func NewCommand() *cli.Command { 8 | return &cli.Command{ 9 | Name: "hns", 10 | Usage: "Manage Host Networking Service", 11 | Subcommands: []*cli.Command{ 12 | getNetworkCommand(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd/client/hns/get_network.go: -------------------------------------------------------------------------------- 1 | package hns 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rancher/wins/cmd/client/internal" 9 | "github.com/rancher/wins/cmd/outputs" 10 | "github.com/rancher/wins/pkg/panics" 11 | "github.com/rancher/wins/pkg/types" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | var _getNetworkFlags = internal.NewGRPCClientConn( 16 | []cli.Flag{ 17 | &cli.StringFlag{ 18 | Name: "name", 19 | Usage: "[optional] Specifies the HNS network name", 20 | }, 21 | &cli.StringFlag{ 22 | Name: "address", 23 | Usage: "[optional] Specifies the HNS network subnet address CIDR", 24 | }, 25 | }, 26 | ) 27 | 28 | var _getNetworkRequest *types.HnsGetNetworkRequest 29 | 30 | func _getNetworkRequestParser(cliCtx *cli.Context) error { 31 | // validate 32 | var ( 33 | name = cliCtx.String("name") 34 | address = cliCtx.String("address") 35 | ) 36 | if name == "" && address == "" { 37 | return errors.New("specifies --name or --address") 38 | } 39 | if name != "" && address != "" { 40 | return errors.New("--name and --address could not use together") 41 | } 42 | 43 | // parse 44 | _getNetworkRequest = &types.HnsGetNetworkRequest{} 45 | if name != "" { 46 | _getNetworkRequest.Options = &types.HnsGetNetworkRequest_Name{ 47 | Name: name, 48 | } 49 | } else if address != "" { 50 | if !strings.Contains(address, "/") { 51 | return errors.New("--address should be a CIDR format") 52 | } 53 | _getNetworkRequest.Options = &types.HnsGetNetworkRequest_Address{ 54 | Address: address, 55 | } 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func _getNetworkAction(cliCtx *cli.Context) (err error) { 62 | defer panics.Log() 63 | 64 | ctx, cancel := context.WithCancel(context.Background()) 65 | defer cancel() 66 | 67 | // parse grpc client connection 68 | grpcClientConn, err := internal.ParseGRPCClientConn(cliCtx) 69 | if err != nil { 70 | return err 71 | } 72 | defer func() { 73 | closeErr := grpcClientConn.Close() 74 | if err == nil { 75 | err = closeErr 76 | } 77 | }() 78 | 79 | // start client 80 | client := types.NewHnsServiceClient(grpcClientConn) 81 | 82 | resp, err := client.GetNetwork(ctx, _getNetworkRequest) 83 | if err != nil { 84 | return 85 | } 86 | 87 | return outputs.JSON(cliCtx.App.Writer, resp.Data) 88 | } 89 | 90 | func getNetworkCommand() *cli.Command { 91 | return &cli.Command{ 92 | Name: "get-network", 93 | Usage: "Get HNS network metadata", 94 | Flags: _getNetworkFlags, 95 | Before: _getNetworkRequestParser, 96 | Action: _getNetworkAction, 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /cmd/client/host/cmd.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func NewCommand() *cli.Command { 8 | return &cli.Command{ 9 | Name: "hst", 10 | Aliases: []string{"host"}, 11 | Usage: "Manage Host", 12 | Subcommands: []*cli.Command{ 13 | getVersionCommand(), 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/client/host/get_version.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rancher/wins/cmd/client/internal" 7 | "github.com/rancher/wins/cmd/outputs" 8 | "github.com/rancher/wins/pkg/panics" 9 | "github.com/rancher/wins/pkg/types" 10 | "github.com/urfave/cli/v2" 11 | ) 12 | 13 | var _getVersionFlags = internal.NewGRPCClientConn([]cli.Flag{}) 14 | 15 | func _getVersionAction(cliCtx *cli.Context) (err error) { 16 | defer panics.Log() 17 | 18 | ctx, cancel := context.WithCancel(context.Background()) 19 | defer cancel() 20 | 21 | // parse grpc client connection 22 | grpcClientConn, err := internal.ParseGRPCClientConn(cliCtx) 23 | if err != nil { 24 | return err 25 | } 26 | defer func() { 27 | closeErr := grpcClientConn.Close() 28 | if err == nil { 29 | err = closeErr 30 | } 31 | }() 32 | 33 | // start client 34 | client := types.NewHostServiceClient(grpcClientConn) 35 | 36 | resp, err := client.GetVersion(ctx, &types.Void{}) 37 | if err != nil { 38 | return 39 | } 40 | 41 | return outputs.JSON(cliCtx.App.Writer, resp.Data) 42 | } 43 | 44 | func getVersionCommand() *cli.Command { 45 | return &cli.Command{ 46 | Name: "get-version", 47 | Usage: "Get host version", 48 | Flags: _getVersionFlags, 49 | Action: _getVersionAction, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cmd/client/internal/grpc.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/rancher/wins/cmd/cmds" 8 | "github.com/rancher/wins/pkg/defaults" 9 | "github.com/rancher/wins/pkg/npipes" 10 | "github.com/urfave/cli/v2" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/credentials/insecure" 13 | ) 14 | 15 | func NewGRPCClientConn(prependFlags []cli.Flag) []cli.Flag { 16 | prependFlags = append(prependFlags, 17 | []cli.Flag{ 18 | &cli.StringFlag{ 19 | Name: "server", 20 | Usage: "[optional] Specifies the name of the server listening named pipe", 21 | Value: defaults.NamedPipeName, 22 | }, 23 | }..., 24 | ) 25 | return cmds.JoinFlags(prependFlags) 26 | } 27 | 28 | func ParseGRPCClientConn(cliCtx *cli.Context) (*grpc.ClientConn, error) { 29 | dialOptions := []grpc.DialOption{ 30 | grpc.WithTransportCredentials(insecure.NewCredentials()), 31 | } 32 | 33 | // setup dialer 34 | server := cliCtx.String("server") 35 | serverPath := npipes.GetFullPath(server) 36 | npipeDialer, err := npipes.NewDialer(serverPath, 5*time.Minute) 37 | if err != nil { 38 | return nil, errors.Wrapf(err, "failed to connect %s", serverPath) 39 | } 40 | dialOptions = append(dialOptions, 41 | grpc.WithContextDialer(npipeDialer), 42 | ) 43 | 44 | // dial server 45 | grpcClientConn, err := grpc.Dial(serverPath, dialOptions...) 46 | if err != nil { 47 | return nil, errors.Wrapf(err, "failed to connect with %s", serverPath) 48 | } 49 | 50 | return grpcClientConn, nil 51 | } 52 | -------------------------------------------------------------------------------- /cmd/client/network/cmd.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func NewCommand() *cli.Command { 8 | return &cli.Command{ 9 | Name: "net", 10 | Aliases: []string{"network"}, 11 | Usage: "Manage Network Adapter", 12 | Subcommands: []*cli.Command{ 13 | getCommand(), 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/client/network/get.go: -------------------------------------------------------------------------------- 1 | package network 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/rancher/wins/cmd/client/internal" 8 | "github.com/rancher/wins/cmd/outputs" 9 | "github.com/rancher/wins/pkg/panics" 10 | "github.com/rancher/wins/pkg/types" 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | var _getFlags = internal.NewGRPCClientConn( 15 | []cli.Flag{ 16 | &cli.StringFlag{ 17 | Name: "name", 18 | Usage: "[optional] Specifies the network name", 19 | }, 20 | &cli.StringFlag{ 21 | Name: "address", 22 | Usage: "[optional] Specifies the network address", 23 | }, 24 | }, 25 | ) 26 | 27 | var _getRequest *types.NetworkGetRequest 28 | 29 | func _getRequestParser(cliCtx *cli.Context) error { 30 | // validate 31 | var ( 32 | name = cliCtx.String("name") 33 | address = cliCtx.String("address") 34 | ) 35 | if name != "" && address != "" { 36 | return errors.New("--name and --address could not use together") 37 | } 38 | 39 | // parse 40 | _getRequest = &types.NetworkGetRequest{} 41 | if name != "" { 42 | _getRequest.Options = &types.NetworkGetRequest_Name{ 43 | Name: name, 44 | } 45 | } else if address != "" { 46 | _getRequest.Options = &types.NetworkGetRequest_Address{ 47 | Address: address, 48 | } 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func _getAction(cliCtx *cli.Context) (err error) { 55 | defer panics.Log() 56 | 57 | ctx, cancel := context.WithCancel(context.Background()) 58 | defer cancel() 59 | 60 | // parse grpc client connection 61 | grpcClientConn, err := internal.ParseGRPCClientConn(cliCtx) 62 | if err != nil { 63 | return err 64 | } 65 | defer func() { 66 | closeErr := grpcClientConn.Close() 67 | if err == nil { 68 | err = closeErr 69 | } 70 | }() 71 | 72 | // start client 73 | client := types.NewNetworkServiceClient(grpcClientConn) 74 | 75 | resp, err := client.Get(ctx, _getRequest) 76 | if err != nil { 77 | return 78 | } 79 | 80 | return outputs.JSON(cliCtx.App.Writer, resp.Data) 81 | } 82 | 83 | func getCommand() *cli.Command { 84 | return &cli.Command{ 85 | Name: "get", 86 | Usage: "Get network metadata", 87 | Flags: _getFlags, 88 | Before: _getRequestParser, 89 | Action: _getAction, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /cmd/client/process/cmd.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func NewCommand() *cli.Command { 8 | return &cli.Command{ 9 | Name: "prc", 10 | Aliases: []string{"process"}, 11 | Usage: "Manage Processes", 12 | Subcommands: []*cli.Command{ 13 | runCommand(), 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /cmd/client/proxy/cmd.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/pkg/defaults" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func NewCommand() *cli.Command { 11 | return &cli.Command{ 12 | Name: "proxy", 13 | Usage: fmt.Sprintf("Set up a proxy for a port via %s (note: only TCP is supported)", defaults.WindowsServiceDisplayName), 14 | Flags: _proxyFlags, 15 | Before: _proxyRequestParser, 16 | Action: _proxyAction, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cmd/client/proxy/publish.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/rancher/remotedialer" 13 | "github.com/rancher/wins/cmd/cmds/flags" 14 | "github.com/rancher/wins/pkg/defaults" 15 | "github.com/rancher/wins/pkg/npipes" 16 | "github.com/rancher/wins/pkg/panics" 17 | "github.com/rancher/wins/pkg/proxy" 18 | "github.com/urfave/cli/v2" 19 | ) 20 | 21 | var _proxyFlags = []cli.Flag{ 22 | &cli.GenericFlag{ 23 | Name: "publish", 24 | Usage: "[required] [list-argument] Publish a port or a range of ports, e.g.: TCP:443 TCP:80-81 (note: only TCP is supported)", 25 | Value: flags.NewListValue(), 26 | }, 27 | &cli.StringFlag{ 28 | Name: "proxy", 29 | Usage: "[optional] Specifies the name of the proxy listening named pipe", 30 | Value: defaults.ProxyPipeName, 31 | }, 32 | } 33 | 34 | var _proxyPorts []int 35 | 36 | func _proxyRequestParser(cliCtx *cli.Context) (err error) { 37 | // Check if ports are provided 38 | publishList := flags.GetListValue(cliCtx, "publish") 39 | if publishList.IsEmpty() { 40 | return fmt.Errorf("No ports to publish") 41 | } 42 | 43 | // Parse ports 44 | publishPorts, err := publishList.Get() 45 | if err != nil { 46 | return fmt.Errorf("failed to parse --publish: %v", err) 47 | } 48 | ports, err := parsePublishes(publishPorts) 49 | if err != nil { 50 | return fmt.Errorf("failed to parse --publish %s: %v", publishPorts, err) 51 | } 52 | _proxyPorts = ports 53 | 54 | return nil 55 | } 56 | 57 | func parsePublishes(publishPorts []string) (ports []int, err error) { 58 | for _, pub := range publishPorts { 59 | publishes := strings.SplitN(pub, ":", 2) 60 | if len(publishes) != 2 { 61 | return nil, fmt.Errorf("could not parse publish %s", publishes) 62 | } 63 | 64 | // TODO(aiyengar2): expand support for UDP ports if an alternative to tcpproxy exists for UDP 65 | protocol := publishes[0] 66 | if protocol != "TCP" { 67 | return nil, fmt.Errorf("unsupported protocol %s, only TCP is supported", protocol) 68 | } 69 | 70 | portRanges := strings.SplitN(publishes[1], "-", 2) 71 | if len(portRanges) == 1 { 72 | number, err := strconv.ParseUint(portRanges[0], 10, 16) 73 | if err != nil { 74 | return nil, fmt.Errorf("could not parse port %s from expose %s: %v", portRanges[0], pub, err) 75 | } 76 | ports = append(ports, int(number)) 77 | continue 78 | } 79 | 80 | if len(portRanges) == 2 { 81 | low, err := strconv.ParseUint(portRanges[0], 10, 16) 82 | if err != nil { 83 | return nil, errors.Wrapf(err, "could not parse port %s from expose %s", portRanges[0], pub) 84 | } 85 | high, err := strconv.ParseUint(portRanges[1], 10, 16) 86 | if err != nil { 87 | return nil, errors.Wrapf(err, "could not parse port %s from expose %s", portRanges[1], pub) 88 | } 89 | if low >= high { 90 | return nil, fmt.Errorf("could not accept the range %d - %d from expose %s", low, high, pub) 91 | } 92 | for number := low; number <= high; number++ { 93 | ports = append(ports, int(number)) 94 | } 95 | continue 96 | } 97 | // port range has invalid format 98 | return nil, fmt.Errorf("could not parse expose %s", pub) 99 | } 100 | 101 | return ports, nil 102 | } 103 | 104 | func _proxyAction(cliCtx *cli.Context) (err error) { 105 | defer panics.Log() 106 | 107 | // Get hostname to identify backend connection 108 | hostname, err := os.Hostname() 109 | if err != nil { 110 | return errors.Wrap(err, "unable to get hostname") 111 | } 112 | proxyHeaders := http.Header{} 113 | proxyHeaders.Set(proxy.ClientIDHeader, hostname) 114 | 115 | // Set up proxy 116 | ctx := context.Background() 117 | pipe := cliCtx.String("proxy") 118 | pipePath := npipes.GetFullPath(pipe) 119 | connAuth := proxy.GetClientConnectAuthorizer(_proxyPorts) 120 | dialer, err := proxy.NewClientDialer(pipePath) 121 | if err != nil { 122 | return fmt.Errorf("Unable to get dialer to named pipe: %v", err) 123 | } 124 | onConn := proxy.GetClientOnConnect(_proxyPorts) 125 | 126 | return remotedialer.ConnectToProxy(ctx, fmt.Sprintf("ws://%s", pipe), proxyHeaders, connAuth, dialer, onConn) 127 | } 128 | -------------------------------------------------------------------------------- /cmd/client/route/add.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | "github.com/rancher/wins/cmd/client/internal" 10 | "github.com/rancher/wins/cmd/cmds/flags" 11 | "github.com/rancher/wins/pkg/panics" 12 | "github.com/rancher/wins/pkg/types" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | var _addFlags = internal.NewGRPCClientConn( 17 | []cli.Flag{ 18 | &cli.GenericFlag{ 19 | Name: "addresses", 20 | Usage: "[required] [list-argument] Specifies the addresses or CIDRs as the destinations, e.g.: 8.8.8.8 6.6.6.6/32", 21 | Value: flags.NewListValue(), 22 | }, 23 | }, 24 | ) 25 | 26 | var _addRequest *types.RouteAddRequest 27 | 28 | func _addRequestParser(cliCtx *cli.Context) error { 29 | // validate 30 | addressList := flags.GetListValue(cliCtx, "addresses") 31 | if addressList.IsEmpty() { 32 | return errors.Errorf("--addresses is required") 33 | } 34 | 35 | // parse 36 | var err error 37 | _addRequest = &types.RouteAddRequest{} 38 | _addRequest.Addresses, err = addressList.Get() 39 | if err != nil { 40 | return errors.Wrap(err, "failed to parse --addresses") 41 | } 42 | for idx, address := range _addRequest.Addresses { 43 | if !strings.Contains(address, "/") { 44 | _addRequest.Addresses[idx] = fmt.Sprintf("%s/32", address) 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func _addAction(cliCtx *cli.Context) (err error) { 52 | defer panics.Log() 53 | 54 | ctx, cancel := context.WithCancel(context.Background()) 55 | defer cancel() 56 | 57 | // parse grpc client connection 58 | grpcClientConn, err := internal.ParseGRPCClientConn(cliCtx) 59 | if err != nil { 60 | return err 61 | } 62 | defer func() { 63 | closeErr := grpcClientConn.Close() 64 | if err == nil { 65 | err = closeErr 66 | } 67 | }() 68 | 69 | // start client 70 | client := types.NewRouteServiceClient(grpcClientConn) 71 | 72 | _, err = client.Add(ctx, _addRequest) 73 | 74 | return 75 | } 76 | 77 | func addCommand() *cli.Command { 78 | return &cli.Command{ 79 | Name: "add", 80 | Usage: "Add a route", 81 | Flags: _addFlags, 82 | Before: _addRequestParser, 83 | Action: _addAction, 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /cmd/client/route/cmd.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func NewCommand() *cli.Command { 8 | return &cli.Command{ 9 | Name: "route", 10 | Usage: "Manage Routes", 11 | Subcommands: []*cli.Command{ 12 | addCommand(), 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd/cmds/flags/list_value.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | type ListValue string 10 | 11 | func (f *ListValue) Set(value string) error { 12 | *f = ListValue(value) 13 | return nil 14 | } 15 | 16 | func (f *ListValue) String() string { 17 | return string(*f) 18 | } 19 | 20 | func (f *ListValue) Get() ([]string, error) { 21 | if f == nil || f.IsEmpty() { 22 | return nil, nil 23 | } 24 | 25 | // Split the string by spaces, but respect escaped quotes 26 | var ret []string 27 | inEscapedDoubleQuotes := false 28 | inEscapedSingleQuotes := false 29 | currVal := "" 30 | for _, c := range f.String() { 31 | if string(c) == "\"" && !inEscapedSingleQuotes { 32 | // toggle if not in single quotes 33 | inEscapedDoubleQuotes = !inEscapedDoubleQuotes 34 | } 35 | if string(c) == "'" && !inEscapedDoubleQuotes { 36 | // toggle if not in single quotes 37 | inEscapedSingleQuotes = !inEscapedSingleQuotes 38 | } 39 | if string(c) == " " && !inEscapedDoubleQuotes && !inEscapedSingleQuotes { 40 | // found an item to add the the list of args 41 | ret = append(ret, currVal) 42 | currVal = "" 43 | continue // do not add the space to the entry 44 | } 45 | currVal += string(c) 46 | } 47 | // Check if input is malformed for the last entry 48 | if inEscapedDoubleQuotes || inEscapedSingleQuotes { 49 | return nil, fmt.Errorf("malformed ListValue contains an unpaired escaped quote") 50 | } 51 | // Add the final field 52 | ret = append(ret, currVal) 53 | return ret, nil 54 | } 55 | 56 | func (f *ListValue) IsEmpty() bool { 57 | if f == nil { 58 | return true 59 | } 60 | return f.String() == "" 61 | } 62 | 63 | func NewListValue() cli.Generic { 64 | return new(ListValue) 65 | } 66 | 67 | func GetListValue(cliCtx *cli.Context, name string) *ListValue { 68 | return cliCtx.Generic(name).(*ListValue) 69 | } 70 | -------------------------------------------------------------------------------- /cmd/cmds/flags/list_value_test.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestNormal(t *testing.T) { 9 | val := ListValue("RANCHER=hello WINS=world") 10 | valList, err := val.Get() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | // Check outputted values 15 | expectedList := []string{"RANCHER=hello", "WINS=world"} 16 | err = fmt.Errorf("Failed to parse list value: expected %v, got %v", expectedList, valList) 17 | for i, expected := range expectedList { 18 | if valList[i] != expected { 19 | t.Fatal(err) 20 | } 21 | } 22 | } 23 | 24 | func TestEscapedDoubleQuotes(t *testing.T) { 25 | val := ListValue("NICK=bye LUTHER=\"hello\" RANCHER=\"hello world\" WINS=world") 26 | valList, err := val.Get() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | // Check outputted values 31 | expectedList := []string{"NICK=bye", "LUTHER=\"hello\"", "RANCHER=\"hello world\"", "WINS=world"} 32 | err = fmt.Errorf("Failed to parse list value: expected %v, got %v", expectedList, valList) 33 | for i, expected := range expectedList { 34 | if valList[i] != expected { 35 | t.Fatal(err) 36 | } 37 | } 38 | } 39 | 40 | func TestEscapedSingleQuotes(t *testing.T) { 41 | val := ListValue("NICK=bye LUTHER='hello' RANCHER='hello world' WINS=world") 42 | valList, err := val.Get() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | // Check outputted values 47 | expectedList := []string{"NICK=bye", "LUTHER='hello'", "RANCHER='hello world'", "WINS=world"} 48 | err = fmt.Errorf("Failed to parse list value: expected %v, got %v", expectedList, valList) 49 | for i, expected := range expectedList { 50 | if valList[i] != expected { 51 | t.Fatal(err) 52 | } 53 | } 54 | } 55 | 56 | func TestEscapedSingleAndDoubleQuotes(t *testing.T) { 57 | val := ListValue("NICK=bye LUTHER=\"hello\" RANCHER='hello world' WINS=world") 58 | valList, err := val.Get() 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | // Check outputted values 63 | expectedList := []string{"NICK=bye", "LUTHER=\"hello\"", "RANCHER='hello world'", "WINS=world"} 64 | err = fmt.Errorf("Failed to parse list value: expected %v, got %v", expectedList, valList) 65 | for i, expected := range expectedList { 66 | if valList[i] != expected { 67 | t.Fatal(err) 68 | } 69 | } 70 | } 71 | 72 | func TestEscapedQuotesInQuotes(t *testing.T) { 73 | val := ListValue("NICK=bye LUTHER=\"'hello'\" RANCHER='\"hello world\"' WINS=world") 74 | valList, err := val.Get() 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | // Check outputted values 79 | expectedList := []string{"NICK=bye", "LUTHER=\"'hello'\"", "RANCHER='\"hello world\"'", "WINS=world"} 80 | err = fmt.Errorf("Failed to parse list value: expected %v, got %v", expectedList, valList) 81 | for i, expected := range expectedList { 82 | if valList[i] != expected { 83 | t.Fatal(err) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/cmds/tools.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func BoolAddr(b bool) *bool { 8 | boolVar := b 9 | return &boolVar 10 | } 11 | 12 | func JoinFlags(flagSlices []cli.Flag) []cli.Flag { 13 | var ret []cli.Flag 14 | for _, flags := range flagSlices { 15 | ret = append(ret, flags) 16 | } 17 | return ret 18 | } 19 | 20 | func ChainFuncs(funcs ...func(*cli.Context) error) func(*cli.Context) error { 21 | if len(funcs) == 0 { 22 | return nil 23 | } 24 | 25 | return func(cliCtx *cli.Context) error { 26 | for _, fn := range funcs { 27 | if fn == nil { 28 | continue 29 | } 30 | 31 | err := fn(cliCtx) 32 | if err != nil { 33 | return err 34 | } 35 | } 36 | 37 | return nil 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /cmd/grpcs/log.go: -------------------------------------------------------------------------------- 1 | package grpcs 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | "strings" 8 | "time" 9 | 10 | grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" 11 | "github.com/sirupsen/logrus" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | func LogrusStreamServerInterceptor() grpc.StreamServerInterceptor { 18 | logEntry := logrus.NewEntry(logrus.StandardLogger()) 19 | 20 | return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 21 | requestStart := time.Now() 22 | responseErr := handler(srv, stream) 23 | responseCode := status.Code(responseErr) 24 | 25 | logEntry.Logln( 26 | grpc_logrus.DefaultCodeToLevel(responseCode), 27 | createLog(stream.Context(), info, requestStart, responseCode, responseErr), 28 | ) 29 | 30 | return responseErr 31 | } 32 | } 33 | 34 | func LogrusUnaryServerInterceptor() grpc.UnaryServerInterceptor { 35 | logEntry := logrus.NewEntry(logrus.StandardLogger()) 36 | 37 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 38 | requestStart := time.Now() 39 | response, responseErr := handler(ctx, req) 40 | responseCode := status.Code(responseErr) 41 | 42 | logEntry.Logln( 43 | grpc_logrus.DefaultCodeToLevel(responseCode), 44 | createLog(ctx, info, requestStart, responseCode, responseErr), 45 | ) 46 | 47 | return response, responseErr 48 | } 49 | } 50 | 51 | func createLog(ctx context.Context, serverInfo interface{}, requestStart time.Time, responseCode codes.Code, responseErr error) string { 52 | sb := &strings.Builder{} 53 | 54 | var methodDescriptor string 55 | switch si := serverInfo.(type) { 56 | case *grpc.StreamServerInfo: 57 | sb.WriteString(fmt.Sprintf("[GRPC - Stream] { %s },", responseCode)) 58 | methodDescriptor = si.FullMethod 59 | case *grpc.UnaryServerInfo: 60 | sb.WriteString(fmt.Sprintf("[GRPC - Unary ] { %s },", responseCode)) 61 | methodDescriptor = si.FullMethod 62 | } 63 | 64 | duration := time.Since(requestStart) 65 | service := path.Dir(methodDescriptor)[1:] 66 | method := path.Base(methodDescriptor) 67 | sb.WriteString(fmt.Sprintf(" %s - %s, cost %v", service, method, duration)) 68 | 69 | if deadline, ok := ctx.Deadline(); ok { 70 | sb.WriteString(", request deadline " + deadline.Format(time.RFC3339)) 71 | } 72 | 73 | if responseErr != nil { 74 | s := status.Convert(responseErr) 75 | sb.WriteString(": " + s.Message()) 76 | } 77 | 78 | return sb.String() 79 | } 80 | -------------------------------------------------------------------------------- /cmd/grpcs/process_path_whitelist.go: -------------------------------------------------------------------------------- 1 | package grpcs 2 | 3 | import ( 4 | "context" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/rancher/wins/pkg/types" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | func ProcessPathUnaryServerInterceptor(whitelist []string) grpc.UnaryServerInterceptor { 15 | whitelistIndex := make(map[string]struct{}, len(whitelist)) 16 | for _, wl := range whitelist { 17 | path := strings.ToLower(filepath.Clean(wl)) 18 | whitelistIndex[path] = struct{}{} 19 | } 20 | 21 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { 22 | if info.FullMethod == "/wins.ProcessService/Start" { 23 | if psr, ok := req.(*types.ProcessStartRequest); ok { 24 | path := strings.ToLower(filepath.Clean(psr.Path)) 25 | if _, exist := whitelistIndex[path]; !exist { 26 | return nil, status.Errorf(codes.InvalidArgument, "invalid path") 27 | } 28 | } 29 | } 30 | 31 | return handler(ctx, req) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/rancher/wins/cmd/stackdump" 10 | 11 | "github.com/mattn/go-colorable" 12 | "github.com/rancher/wins/cmd/client" 13 | "github.com/rancher/wins/cmd/server" 14 | "github.com/rancher/wins/pkg/defaults" 15 | "github.com/rancher/wins/pkg/panics" 16 | "github.com/sirupsen/logrus" 17 | "github.com/urfave/cli/v2" 18 | ) 19 | 20 | func main() { 21 | defer panics.Log() 22 | 23 | app := cli.NewApp() 24 | app.Version = defaults.AppVersion 25 | app.Name = defaults.WindowsServiceName 26 | app.Usage = "A way to operate the Windows host inside the Windows container" 27 | app.Description = fmt.Sprintf(`%s Component (%s)`, defaults.WindowsServiceDisplayName, defaults.AppCommit) 28 | app.Writer = colorable.NewColorableStdout() 29 | app.ErrWriter = colorable.NewColorableStderr() 30 | app.CommandNotFound = func(cliCtx *cli.Context, s string) { 31 | _, err := fmt.Fprintf(cliCtx.App.Writer, "Invalid Command: %s \n\n", s) 32 | if err != nil { 33 | return 34 | } 35 | if pcliCtx := cliCtx.Lineage(); pcliCtx[1] == nil { 36 | cli.ShowAppHelpAndExit(cliCtx, 1) 37 | } else { 38 | cli.ShowCommandHelpAndExit(cliCtx, cliCtx.Command.Name, 1) 39 | } 40 | } 41 | app.OnUsageError = func(cliCtx *cli.Context, err error, isSubcommand bool) error { 42 | _, err = fmt.Fprintf(cliCtx.App.Writer, "Incorrect Usage: %s \n\n", err.Error()) 43 | if err != nil { 44 | return err 45 | } 46 | if isSubcommand { 47 | err := cli.ShowSubcommandHelp(cliCtx) 48 | if err != nil { 49 | return err 50 | } 51 | } else { 52 | err := cli.ShowAppHelp(cliCtx) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | return nil 58 | } 59 | app.Before = func(cliCtx *cli.Context) error { 60 | logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true, FullTimestamp: true}) 61 | logrus.SetOutput(cliCtx.App.Writer) 62 | return nil 63 | } 64 | 65 | app.Commands = []*cli.Command{ 66 | server.NewCommand(), 67 | client.NewCommand(), 68 | stackdump.NewCommand(), 69 | } 70 | 71 | app.Flags = []cli.Flag{ 72 | &cli.BoolFlag{ 73 | Name: "debug", 74 | Usage: "Turn on verbose debug logging", 75 | }, 76 | &cli.BoolFlag{ 77 | Name: "quiet", 78 | Usage: "Turn off all logging", 79 | }, 80 | } 81 | 82 | app.Before = func(c *cli.Context) error { 83 | if c.Bool("debug") { 84 | logrus.SetLevel(logrus.DebugLevel) 85 | } 86 | if c.Bool("quiet") { 87 | logrus.SetOutput(ioutil.Discard) 88 | } 89 | return nil 90 | } 91 | 92 | if err := app.Run(os.Args); err != nil && err != io.EOF { 93 | logrus.Fatal(err) 94 | } 95 | 96 | logrus.Debug("Finished") 97 | } 98 | -------------------------------------------------------------------------------- /cmd/outputs/response.go: -------------------------------------------------------------------------------- 1 | package outputs 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rancher/wins/pkg/converters" 9 | ) 10 | 11 | func JSON(w io.Writer, obj interface{}) error { 12 | if obj == nil { 13 | return nil 14 | } 15 | 16 | bytes, ok := obj.([]byte) 17 | if ok { 18 | if len(bytes) == 0 { 19 | return nil 20 | } 21 | return fprint(w, converters.UnsafeBytesToString(bytes)) 22 | } 23 | 24 | json, err := converters.ToJSON(obj) 25 | if err != nil { 26 | return err 27 | } 28 | return fprint(w, json) 29 | } 30 | 31 | func fprint(w io.Writer, obj interface{}) (err error) { 32 | _, err = fmt.Fprint(w, obj) 33 | if err != nil { 34 | err = errors.Wrap(err, "failed to output result") 35 | } 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /cmd/server/app/cmd.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/pkg/defaults" 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | func NewCommand() *cli.Command { 11 | return &cli.Command{ 12 | Name: "app", 13 | Aliases: []string{"application"}, 14 | Usage: fmt.Sprintf("Manage %s Application", defaults.WindowsServiceDisplayName), 15 | Subcommands: []*cli.Command{ 16 | runCommand(), 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/server/app/run.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rancher/wins/cmd/server/config" 9 | "github.com/rancher/wins/pkg/apis" 10 | "github.com/rancher/wins/pkg/csiproxy" 11 | "github.com/rancher/wins/pkg/defaults" 12 | "github.com/rancher/wins/pkg/panics" 13 | "github.com/rancher/wins/pkg/profilings" 14 | "github.com/rancher/wins/pkg/systemagent" 15 | "github.com/sirupsen/logrus" 16 | "github.com/urfave/cli/v2" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | var _runFlags = []cli.Flag{ 21 | &cli.BoolFlag{ 22 | Name: "register", 23 | Usage: "[optional] Register to the Windows Service", 24 | }, 25 | &cli.BoolFlag{ 26 | Name: "unregister", 27 | Usage: "[optional] Unregister from the Windows Service", 28 | }, 29 | &cli.StringFlag{ 30 | Name: "config", 31 | Usage: "[optional] Specifies the path of the configuration", 32 | Value: defaults.ConfigPath, 33 | }, 34 | &cli.StringFlag{ 35 | Name: "profile", 36 | Usage: "[optional] Specifies the name of profile to capture (none|cpu|heap|goroutine|threadcreate|block|mutex)", 37 | Value: "none", 38 | }, 39 | &cli.StringFlag{ 40 | Name: "profile-output", 41 | Usage: "[optional] Specifies the name of the file to write the profile to", 42 | Value: "profile.pprof", 43 | }, 44 | &cli.BoolFlag{ 45 | Name: "delayed-start", 46 | Usage: "[optional] configure the rancher-wins service with a start type of 'Automatic (Delayed)'", 47 | Value: false, 48 | }, 49 | } 50 | 51 | func _profilingInit(cliCtx *cli.Context) error { 52 | return profilings.Init(cliCtx.String("profile"), cliCtx.String("profile-output")) 53 | } 54 | 55 | func _profilingFlush(cliCtx *cli.Context) error { 56 | return profilings.Flush(cliCtx.String("profile"), cliCtx.String("profile-output")) 57 | } 58 | 59 | func _runAction(cliCtx *cli.Context) error { 60 | defer panics.Log() 61 | 62 | ctx, cancel := context.WithCancel(context.Background()) 63 | defer cancel() 64 | 65 | // register / unregister service 66 | register := cliCtx.Bool("register") 67 | unregister := cliCtx.Bool("unregister") 68 | delayedStart := cliCtx.Bool("delayed-start") 69 | if register { 70 | if unregister { 71 | return errors.New("failed to execute: --register and --unregister could not use together") 72 | } 73 | 74 | err := registerService(delayedStart) 75 | if err != nil { 76 | return errors.Wrap(err, "failed to register service") 77 | } 78 | return nil 79 | } 80 | if unregister { 81 | err := unregisterService() 82 | if err != nil { 83 | return errors.Wrap(err, "failed to unregister service") 84 | } 85 | return nil 86 | } 87 | 88 | // parse config 89 | cfg := config.DefaultConfig() 90 | cfgPath := cliCtx.String("config") 91 | err := config.LoadConfig(cfgPath, cfg) 92 | if err != nil { 93 | return errors.Wrapf(err, "failed to load config from %s", cfgPath) 94 | } 95 | 96 | serverOptions := []grpc.ServerOption{ 97 | grpc.ConnectionTimeout(5 * time.Second), 98 | } 99 | 100 | serverOptions, err = setupGRPCServerOptions(serverOptions, cfg) 101 | if err != nil { 102 | return errors.Wrap(err, "failed to setup grpc middlewares") 103 | } 104 | 105 | logrus.Debugf("Proxy port whitelist: %v", cfg.WhiteList.ProxyPorts) 106 | server, err := apis.NewServer(cfg.Listen, serverOptions, cfg.Proxy, cfg.WhiteList.ProxyPorts) 107 | if err != nil { 108 | return errors.Wrap(err, "failed to create server") 109 | } 110 | 111 | // adding system agent 112 | agent := systemagent.New(cfg.SystemAgent) 113 | 114 | // Determine if the agent should use strict verification 115 | agent.StrictTLSMode = cfg.AgentStrictTLSMode 116 | 117 | //checking if CSI Proxy has config, if so enables it. 118 | if cfg.CSIProxy != nil { 119 | logrus.Infof("CSI Proxy will be enabled as a Windows service.") 120 | csi, err := csiproxy.New(cfg.CSIProxy, cfg.TLSConfig) 121 | if err != nil { 122 | return err 123 | } 124 | if err := csi.Enable(); err != nil { 125 | return err 126 | } 127 | } 128 | 129 | err = runService(ctx, server, agent) 130 | if err != nil { 131 | return errors.Wrap(err, "failed to run server") 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func runCommand() *cli.Command { 138 | return &cli.Command{ 139 | Name: "run", 140 | Usage: "Run application", 141 | Flags: _runFlags, 142 | Before: _profilingInit, 143 | Action: _runAction, 144 | After: _profilingFlush, 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /cmd/server/app/run_grpc.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" 5 | "github.com/rancher/wins/cmd/grpcs" 6 | "github.com/rancher/wins/cmd/server/config" 7 | "github.com/sirupsen/logrus" 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | func setupGRPCServerOptions(serverOptions []grpc.ServerOption, cfg *config.Config) ([]grpc.ServerOption, error) { 12 | ui := make([]grpc.UnaryServerInterceptor, 0) 13 | si := make([]grpc.StreamServerInterceptor, 0) 14 | 15 | // add logging middleware 16 | debug := cfg.Debug 17 | if debug { 18 | logrus.SetLevel(logrus.DebugLevel) 19 | ui = append(ui, 20 | grpcs.LogrusUnaryServerInterceptor(), 21 | ) 22 | si = append(si, 23 | grpcs.LogrusStreamServerInterceptor(), 24 | ) 25 | } 26 | 27 | // add process path whitelist middleware 28 | processPathWhiteList := cfg.WhiteList.ProcessPaths 29 | if len(processPathWhiteList) != 0 { 30 | logrus.Debugf("Process path whitelist: %v", processPathWhiteList) 31 | ui = append(ui, 32 | grpcs.ProcessPathUnaryServerInterceptor(processPathWhiteList), 33 | ) 34 | } 35 | 36 | if len(ui) != 0 { 37 | serverOptions = append(serverOptions, 38 | grpc_middleware.WithUnaryServerChain(ui...), 39 | ) 40 | } 41 | if len(si) != 0 { 42 | serverOptions = append(serverOptions, 43 | grpc_middleware.WithStreamServerChain(si...), 44 | ) 45 | } 46 | return serverOptions, nil 47 | } 48 | -------------------------------------------------------------------------------- /cmd/server/cmd.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/cmd/server/app" 7 | "github.com/rancher/wins/pkg/defaults" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | func NewCommand() *cli.Command { 12 | return &cli.Command{ 13 | Name: "srv", 14 | Aliases: []string{"server"}, 15 | Description: fmt.Sprintf("The server side commands of %s", defaults.WindowsServiceDisplayName), 16 | Subcommands: []*cli.Command{ 17 | app.NewCommand(), 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cmd/server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "strings" 8 | 9 | "github.com/ghodss/yaml" 10 | "github.com/pkg/errors" 11 | "github.com/rancher/system-agent/pkg/config" 12 | "github.com/rancher/wins/pkg/csiproxy" 13 | "github.com/rancher/wins/pkg/defaults" 14 | wintls "github.com/rancher/wins/pkg/tls" 15 | ) 16 | 17 | func DefaultConfig() *Config { 18 | return &Config{ 19 | Listen: defaults.NamedPipeName, 20 | Proxy: defaults.ProxyPipeName, 21 | WhiteList: WhiteListConfig{ 22 | ProcessPaths: []string{}, 23 | ProxyPorts: []int{}, 24 | }, 25 | AgentStrictTLSMode: false, 26 | } 27 | } 28 | 29 | type Config struct { 30 | Debug bool `yaml:"debug" json:"debug"` 31 | Listen string `yaml:"listen" json:"listen"` 32 | Proxy string `yaml:"proxy" json:"proxy"` 33 | WhiteList WhiteListConfig `yaml:"white_list" json:"white_list"` 34 | SystemAgent *config.AgentConfig `yaml:"systemagent" json:"systemagent,omitempty"` 35 | AgentStrictTLSMode bool `yaml:"agentStrictTLSMode" json:"agentStrictTLSMode"` 36 | CSIProxy *csiproxy.Config `yaml:"csi-proxy" json:"csi-proxy,omitempty"` 37 | TLSConfig *wintls.Config `yaml:"tls-config" json:"tls-config,omitempty"` 38 | } 39 | 40 | func (c *Config) Validate() error { 41 | if strings.TrimSpace(c.Listen) == "" { 42 | return errors.New("[Validate] listen cannot be blank") 43 | } 44 | 45 | // validate white list field 46 | if err := c.WhiteList.Validate(); err != nil { 47 | return errors.Wrap(err, "[Validate] failed to validate white list field") 48 | } 49 | 50 | return nil 51 | } 52 | 53 | type WhiteListConfig struct { 54 | ProcessPaths []string `yaml:"process_paths" json:"processPaths"` 55 | ProxyPorts []int `yaml:"proxy_ports" json:"proxyPorts"` 56 | } 57 | 58 | func (c *WhiteListConfig) Validate() error { 59 | // process path 60 | for _, processPath := range c.ProcessPaths { 61 | if strings.TrimSpace(processPath) == "" { 62 | return errors.New("could not accept blank path as process white list") 63 | } 64 | } 65 | for _, proxyPort := range c.ProxyPorts { 66 | if proxyPort < 0 || proxyPort > 0xFFFF { 67 | return errors.New("could not accept invalid port number in proxy ports") 68 | } 69 | } 70 | return nil 71 | } 72 | 73 | func LoadConfig(path string, v *Config) error { 74 | if v == nil { 75 | return errors.New("config cannot be nil") 76 | } 77 | 78 | stat, err := os.Stat(path) 79 | if err != nil { 80 | if !os.IsNotExist(err) { 81 | return errors.Wrap(err, "could not load config") 82 | } 83 | return nil 84 | } else if stat.IsDir() { 85 | return errors.New("could not load config from directory") 86 | } 87 | 88 | if err := DecodeConfig(path, v); err != nil { 89 | return errors.Wrap(err, "could not decode config") 90 | } 91 | 92 | return v.Validate() 93 | } 94 | 95 | func SaveConfig(path string, v *Config) error { 96 | if v == nil { 97 | return errors.New("config cannot be nil") 98 | } 99 | 100 | yml, err := yaml.Marshal(v) 101 | if err != nil { 102 | return fmt.Errorf("could not marshal provided config: %w", err) 103 | } 104 | 105 | return os.WriteFile(path, yml, os.ModePerm) 106 | } 107 | 108 | func DecodeConfig(path string, v *Config) error { 109 | bs, err := ioutil.ReadFile(path) 110 | if err != nil { 111 | return err 112 | } 113 | return yaml.Unmarshal(bs, v) 114 | } 115 | -------------------------------------------------------------------------------- /cmd/stackdump/cmd.go: -------------------------------------------------------------------------------- 1 | package stackdump 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | func NewCommand() *cli.Command { 8 | return &cli.Command{ 9 | Name: "stackdump", 10 | Hidden: true, 11 | Action: _stackDumpAction, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /cmd/stackdump/stackdump.go: -------------------------------------------------------------------------------- 1 | package stackdump 2 | 3 | import ( 4 | "github.com/rancher/wins/pkg/profilings" 5 | "github.com/urfave/cli/v2" 6 | ) 7 | 8 | func _stackDumpAction(_ *cli.Context) (err error) { 9 | err = profilings.StackDump() 10 | if err != nil { 11 | return err 12 | } 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /docs/adrs/0001-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 1. Record architecture decisions 2 | 3 | Date: 2021-10-04 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | We need to record the architectural decisions made on this project. 12 | 13 | ## Decision 14 | 15 | We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). 16 | 17 | ## Consequences 18 | 19 | See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools). 20 | -------------------------------------------------------------------------------- /docs/adrs/0002-add-system-agent-support-to-wins.md: -------------------------------------------------------------------------------- 1 | # 2. Add System Agent support to Wins 2 | 3 | Date: 2021-10-04 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | Currently, RKE2 uses [`rancher/system-agent`](https://github.com/rancher/system-agent) to install and upgrade RKE2 using plans. However, this capability doesn't currently exist for Windows hosts. System Agent functionality needs some capabilities of Wins to achieve its goal. This would require that both the system agent service and wins service to be running on a system. Alternatively, we could consume the system agent as a package in the wins service reducing the need to having both installed in a system. 12 | 13 | ## Decision 14 | 15 | The decision was to consume the system agent functionality as a package in wins. 16 | 17 | ## Consequences 18 | 19 | This decision reduces the overall services running on a system and allows leveraging the existing wins upgrader and capabilities to provide the system agent functionality. 20 | -------------------------------------------------------------------------------- /docs/adrs/0003-system-agent-configuration.md: -------------------------------------------------------------------------------- 1 | # 3. System Agent configuration 2 | 3 | Date: 2021-10-04 4 | 5 | ## Status 6 | 7 | Accepted 8 | 9 | ## Context 10 | 11 | The system agent requires some configuration. In addition, wins need the ability to enable or disable the system agent functionality to enable backwards compatibility. 12 | 13 | ## Decision 14 | 15 | Two decisions were made. The system agent functionality was added to the wins configuration file as a section called *sa*. The second decision is that if that section doesn't exist, then system agent functionality will be disabled. If, it does exist, then the system agent functionality will be enabled. 16 | 17 | ## Consequences 18 | 19 | This allows maintaining consistent behavior for existing users. Users will not need any additional command line options to alter the behavior. This does require that users of system agent know that the configuration is required. The primary user currently will be Rancher and therefore, will set the configuration correctly during install. -------------------------------------------------------------------------------- /docs/adrs/0004-csi-proxy-and-configuration.md: -------------------------------------------------------------------------------- 1 | # 4. CSI Proxy and Configuration 2 | 3 | Date: 2021-11-22 4 | 5 | ## Status 6 | 7 | Approved 8 | 9 | ## Context 10 | 11 | To enable storage with Windows nodes, the [CSI Proxy](https://github.com/kubernetes-csi/csi-proxy) was created to allow CSI implementations to run as daemonsets on Windows nodes. Implementing support for installation and configuration of the CSI Proxy within Wins allows multiple projects to benefit from having the ability to have the CSI Proxy available, including both RKE and RKE2. 12 | 13 | ## Decision 14 | 15 | After some discussion, it was decided that Wins would be responsible for downloading the specified version of CSI Proxy from the current upstream location based on the version that is specified. The ability to override the upstream URL will be provided. Once the binary is available Wins will be responsible for creating the Windows Service for CSI Proxy and ensuring that it is running. Wins will also have the ability to manage the lifecycle of the CSI Proxy service since configuration changes will need to be picked up from Wins. The proposed configuration is below: 16 | 17 | ```yaml 18 | csi-proxy: 19 | url: 20 | version: 21 | ``` 22 | 23 | The presence of the configuration will enable the CSI Proxy configuration. 24 | 25 | ## Consequences 26 | 27 | This allows maintaining consistent behavior for existing users. Users will not need any additional command line options to alter the behavior. This does require that users of CSI Proxy know that the configuration is required. The primary user currently will be Rancher and therefore, will set the configuration correctly during install. -------------------------------------------------------------------------------- /magetools/gotool.go: -------------------------------------------------------------------------------- 1 | package magetools 2 | 3 | import ( 4 | "github.com/magefile/mage/sh" 5 | ) 6 | 7 | type Go struct { 8 | Arch string 9 | OS string 10 | Version string 11 | Commit string 12 | CGoEnabled string 13 | Verbose string 14 | } 15 | 16 | func NewGo(arch, goos, version, commit, cgoEnabled, verbose string) *Go { 17 | return &Go{ 18 | Arch: arch, 19 | OS: goos, 20 | Version: version, 21 | Commit: commit, 22 | CGoEnabled: cgoEnabled, 23 | Verbose: verbose, 24 | } 25 | } 26 | 27 | func (g *Go) Build(flags func(string, string) string, target, output string) error { 28 | envs := map[string]string{"GOOS": g.OS, "GOARCH": g.Arch, "CGO_ENABLED": g.CGoEnabled, "MAGEFILE_VERBOSE": g.Verbose} 29 | return sh.RunWithV(envs, "go", "build", "-o", output, "--ldflags="+flags(g.Version, g.Commit), target) 30 | } 31 | 32 | func (g *Go) Test(flags func(string, string) string, target string) error { 33 | envs := map[string]string{"GOOS": g.OS, "GOARCH": g.Arch, "CGO_ENABLED": g.CGoEnabled, "MAGEFILE_VERBOSE": g.Verbose} 34 | return sh.RunWithV(envs, "go", "test", "-v", "-cover", "--ldflags="+flags(g.Version, g.Commit), target) 35 | } 36 | 37 | func (g *Go) Mod(cmd string) error { 38 | envs := map[string]string{"GOOS": g.OS, "GOARCH": g.Arch} 39 | return sh.RunWithV(envs, "go", "mod", cmd) 40 | } 41 | -------------------------------------------------------------------------------- /magetools/helpers.go: -------------------------------------------------------------------------------- 1 | package magetools 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/magefile/mage/sh" 7 | ) 8 | 9 | func IsGitClean() (bool, error) { 10 | result, err := sh.Output("git", "status", "--porcelain", "--untracked-files=no") 11 | if err != nil { 12 | return false, err 13 | } 14 | if result != "" { 15 | return false, nil 16 | } 17 | return true, nil 18 | } 19 | 20 | func GetLatestTag() (string, error) { 21 | result, err := sh.Output("git", "tag", "-l", "--contains", "HEAD") 22 | if err != nil { 23 | return "", err 24 | } 25 | return strings.TrimSpace(result), nil 26 | } 27 | 28 | func GetCommit() (string, error) { 29 | result, err := sh.Output("git", "rev-parse", "--short", "HEAD") 30 | if err != nil { 31 | return "", err 32 | } 33 | return strings.TrimSpace(result), nil 34 | } 35 | -------------------------------------------------------------------------------- /manifest.tmpl: -------------------------------------------------------------------------------- 1 | image: rancher/wins:{{build.tag}} 2 | manifests: 3 | - 4 | image: rancher/wins:{{build.tag}}-windows-1809 5 | platform: 6 | architecture: amd64 7 | os: windows 8 | version: 1809 9 | - 10 | image: rancher/wins:{{build.tag}}-windows-ltsc2022 11 | platform: 12 | architecture: amd64 13 | os: windows 14 | version: ltsc2022 -------------------------------------------------------------------------------- /pkg/apis/api.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/Microsoft/go-winio" 9 | "github.com/hashicorp/go-multierror" 10 | "github.com/pkg/errors" 11 | "github.com/rancher/remotedialer" 12 | "github.com/rancher/wins/pkg/npipes" 13 | "github.com/rancher/wins/pkg/proxy" 14 | "github.com/rancher/wins/pkg/types" 15 | "github.com/sirupsen/logrus" 16 | "golang.org/x/sync/errgroup" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | type Server struct { 21 | listener net.Listener 22 | proxy proxyServer 23 | server *grpc.Server 24 | } 25 | 26 | type proxyServer struct { 27 | listener net.Listener 28 | ports []int 29 | } 30 | 31 | func (s *Server) Close() error { 32 | s.server.Stop() 33 | return multierror.Append(s.listener.Close(), s.proxy.listener.Close()) 34 | } 35 | 36 | func (s *Server) Serve(ctx context.Context) error { 37 | srv := s.server 38 | 39 | // register service 40 | types.RegisterHostServiceServer(srv, &hostService{}) 41 | types.RegisterNetworkServiceServer(srv, &networkService{}) 42 | types.RegisterHnsServiceServer(srv, &hnsService{}) 43 | types.RegisterRouteServiceServer(srv, &routeService{}) 44 | types.RegisterProcessServiceServer(srv, &processService{}) 45 | types.RegisterApplicationServiceServer(srv, &applicationService{}) 46 | 47 | errg, _ := errgroup.WithContext(ctx) 48 | 49 | errg.Go(func() error { 50 | logrus.Infof("Listening on %v", s.listener.Addr()) 51 | return srv.Serve(s.listener) 52 | }) 53 | 54 | errg.Go(func() error { 55 | logrus.Infof("Listening on %v", s.proxy.listener.Addr()) 56 | handler := remotedialer.New(proxy.GetServerAuthorizer(), remotedialer.DefaultErrorWriter) 57 | handler.ClientConnectAuthorizer = proxy.GetClientConnectAuthorizer(s.proxy.ports) 58 | return http.Serve(s.proxy.listener, handler) 59 | }) 60 | 61 | return errg.Wait() 62 | } 63 | 64 | func NewServer(listen string, serverOptions []grpc.ServerOption, proxy string, proxyPorts []int) (*Server, error) { 65 | listenPath := npipes.GetFullPath(listen) 66 | listener, err := npipes.New(listenPath, "", 0) 67 | if err != nil { 68 | return nil, errors.Wrapf(err, "could not listen %s", listenPath) 69 | } 70 | 71 | proxyPath := npipes.GetFullPath(proxy) 72 | path, err := npipes.ParsePath(proxyPath) 73 | if err != nil { 74 | return nil, errors.Wrapf(err, "could not parse path %s", proxyPath) 75 | } 76 | proxyListener, err := winio.ListenPipe(path, nil) 77 | if err != nil { 78 | return nil, errors.Wrapf(err, "could not listen %s", proxyPath) 79 | } 80 | logrus.Infof("listening for tcp requests on %s destined for: %v", proxy, proxyPorts) 81 | 82 | return &Server{ 83 | listener: listener, 84 | proxy: proxyServer{ 85 | listener: proxyListener, 86 | ports: proxyPorts, 87 | }, 88 | server: grpc.NewServer(serverOptions...), 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/apis/application_service.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rancher/wins/pkg/defaults" 9 | "github.com/rancher/wins/pkg/panics" 10 | "github.com/rancher/wins/pkg/paths" 11 | "github.com/rancher/wins/pkg/types" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | type applicationService struct { 17 | } 18 | 19 | func (s *applicationService) Info(_ context.Context, _ *types.Void) (resp *types.ApplicationInfoResponse, respErr error) { 20 | defer panics.DealWith(func(recoverObj interface{}) { 21 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 22 | }) 23 | 24 | info, err := getActualInfo() 25 | if err != nil { 26 | return nil, status.Errorf(codes.Internal, "could not get actual info: %v", err) 27 | } 28 | 29 | return &types.ApplicationInfoResponse{ 30 | Info: info, 31 | }, nil 32 | } 33 | 34 | func getActualInfo() (*types.ApplicationInfo, error) { 35 | serverChecksum, err := paths.GetBinarySHA1Hash(os.Args[0]) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "could not get file checksum") 38 | } 39 | 40 | return &types.ApplicationInfo{ 41 | Checksum: serverChecksum, 42 | Version: defaults.AppVersion, 43 | Commit: defaults.AppCommit, 44 | }, nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/apis/hns_service.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/Microsoft/hcsshim" 7 | "github.com/Microsoft/hcsshim/hcn" 8 | "github.com/rancher/wins/pkg/converters" 9 | "github.com/rancher/wins/pkg/panics" 10 | "github.com/rancher/wins/pkg/types" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | ) 14 | 15 | type hnsService struct { 16 | } 17 | 18 | func (s *hnsService) GetNetwork(_ context.Context, req *types.HnsGetNetworkRequest) (resp *types.HnsGetNetworkResponse, respErr error) { 19 | defer panics.DealWith(func(recoverObj interface{}) { 20 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 21 | }) 22 | 23 | var hnsNetwork *types.HnsNetwork 24 | 25 | // get network 26 | switch opts := req.GetOptions().(type) { 27 | case *types.HnsGetNetworkRequest_Name: 28 | if isV2Api() { 29 | network, err := hcn.GetNetworkByName(opts.Name) 30 | if err != nil { 31 | return nil, status.Errorf(codes.InvalidArgument, "could not get HNS network %s via v2 api: %v", opts.Name, err) 32 | } 33 | hnsNetwork = v2nativeToHnsNetwork(network) 34 | } else { 35 | network, err := hcsshim.GetHNSNetworkByName(opts.Name) 36 | if err != nil { 37 | return nil, status.Errorf(codes.InvalidArgument, "could not get HNS network %s via v1 api: %v", opts.Name, err) 38 | } 39 | hnsNetwork = v1nativeToHnsNetwork(network) 40 | } 41 | case *types.HnsGetNetworkRequest_Address: 42 | if isV2Api() { 43 | network, err := v2getHNSNetworkByAddress(opts.Address) 44 | if err != nil { 45 | return nil, status.Errorf(codes.InvalidArgument, "could not get HNS network %s via v2 api: %v", opts.Address, err) 46 | } 47 | hnsNetwork = v2nativeToHnsNetwork(network) 48 | } else { 49 | network, err := v1getHNSNetworkByAddress(opts.Address) 50 | if err != nil { 51 | return nil, status.Errorf(codes.InvalidArgument, "could not get HNS network %s via v1 api: %v", opts.Address, err) 52 | } 53 | hnsNetwork = v1nativeToHnsNetwork(network) 54 | } 55 | default: 56 | return nil, status.Errorf(codes.InvalidArgument, "indicate the HNS network name or address") 57 | } 58 | 59 | // construct response 60 | return &types.HnsGetNetworkResponse{ 61 | Data: hnsNetwork, 62 | }, nil 63 | } 64 | 65 | func isV2Api() bool { 66 | return hcn.V2ApiSupported() == nil 67 | } 68 | 69 | func v1getHNSNetworkByAddress(address string) (*hcsshim.HNSNetwork, error) { 70 | hnsNetworks, err := hcsshim.HNSListNetworkRequest("GET", "", "") 71 | if err != nil { 72 | return nil, err 73 | } 74 | for _, hnsNetwork := range hnsNetworks { 75 | for _, nativeSubnet := range hnsNetwork.Subnets { 76 | if nativeSubnet.AddressPrefix == address { 77 | return &hnsNetwork, nil 78 | } 79 | } 80 | } 81 | return nil, hcsshim.NetworkNotFoundError{NetworkName: address} 82 | } 83 | 84 | func v2getHNSNetworkByAddress(address string) (*hcn.HostComputeNetwork, error) { 85 | hnsNetworks, err := hcn.ListNetworks() 86 | if err != nil { 87 | return nil, err 88 | } 89 | for _, hnsNetwork := range hnsNetworks { 90 | for _, ipam := range hnsNetwork.Ipams { 91 | for _, nativeSubnet := range ipam.Subnets { 92 | if nativeSubnet.IpAddressPrefix == address { 93 | return &hnsNetwork, nil 94 | } 95 | } 96 | } 97 | } 98 | return nil, hcsshim.NetworkNotFoundError{NetworkName: address} 99 | } 100 | 101 | func v1nativeToHnsNetwork(nativeData *hcsshim.HNSNetwork) *types.HnsNetwork { 102 | var subnets []*types.HnsNetworkSubnet 103 | for _, nativeSubnet := range nativeData.Subnets { 104 | subnets = append(subnets, &types.HnsNetworkSubnet{ 105 | AddressCIDR: nativeSubnet.AddressPrefix, 106 | GatewayAddress: nativeSubnet.GatewayAddress, 107 | }) 108 | } 109 | 110 | return &types.HnsNetwork{ 111 | ID: nativeData.Id, 112 | Type: nativeData.Type, 113 | Subnets: subnets, 114 | ManagementIP: nativeData.ManagementIP, 115 | } 116 | } 117 | 118 | func v2nativeToHnsNetwork(nativeData *hcn.HostComputeNetwork) *types.HnsNetwork { 119 | var subnets []*types.HnsNetworkSubnet 120 | for _, ipam := range nativeData.Ipams { 121 | for _, nativeSubnet := range ipam.Subnets { 122 | subnets = append(subnets, &types.HnsNetworkSubnet{ 123 | AddressCIDR: nativeSubnet.IpAddressPrefix, 124 | GatewayAddress: nativeSubnet.Routes[0].NextHop, 125 | }) 126 | } 127 | } 128 | 129 | var managementIP string 130 | for _, policy := range nativeData.Policies { 131 | if policy.Type == hcn.ProviderAddress { 132 | managementIP = converters.GetStringFormJSON(policy.Settings, "ProviderAddress") 133 | } 134 | } 135 | 136 | return &types.HnsNetwork{ 137 | ID: nativeData.Id, 138 | Type: string(nativeData.Type), 139 | Subnets: subnets, 140 | ManagementIP: managementIP, 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /pkg/apis/host_service.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/rancher/wins/pkg/converters" 7 | "github.com/rancher/wins/pkg/panics" 8 | "github.com/rancher/wins/pkg/types" 9 | "golang.org/x/sys/windows/registry" 10 | "google.golang.org/grpc/codes" 11 | "google.golang.org/grpc/status" 12 | ) 13 | 14 | type hostService struct { 15 | } 16 | 17 | func (s *hostService) GetVersion(_ context.Context, _ *types.Void) (resp *types.HostGetVersionResponse, respErr error) { 18 | defer panics.DealWith(func(recoverObj interface{}) { 19 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 20 | }) 21 | 22 | currentVersionRegKey, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) 23 | if err != nil { 24 | return nil, status.Errorf(codes.Internal, "could not open registry key: %v", err) 25 | } 26 | defer currentVersionRegKey.Close() 27 | 28 | // construct response 29 | return &types.HostGetVersionResponse{ 30 | Data: registryKeyToHostVersion(currentVersionRegKey), 31 | }, nil 32 | } 33 | 34 | func registryKeyToHostVersion(k registry.Key) *types.HostVersion { 35 | return &types.HostVersion{ 36 | CurrentMajorVersionNumber: converters.GetIntStringFormRegistryKey(k, "CurrentMajorVersionNumber"), 37 | CurrentMinorVersionNumber: converters.GetIntStringFormRegistryKey(k, "CurrentMinorVersionNumber"), 38 | CurrentBuildNumber: converters.GetStringFromRegistryKey(k, "CurrentBuildNumber"), 39 | UBR: converters.GetIntStringFormRegistryKey(k, "UBR"), 40 | ReleaseId: converters.GetStringFromRegistryKey(k, "ReleaseId"), 41 | BuildLabEx: converters.GetStringFromRegistryKey(k, "BuildLabEx"), 42 | CurrentBuild: converters.GetStringFromRegistryKey(k, "CurrentBuild"), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/apis/network_service.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "syscall" 10 | "unsafe" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/rancher/wins/pkg/converters" 14 | "github.com/rancher/wins/pkg/panics" 15 | "github.com/rancher/wins/pkg/syscalls" 16 | "github.com/rancher/wins/pkg/types" 17 | "golang.org/x/sys/windows" 18 | "google.golang.org/grpc/codes" 19 | "google.golang.org/grpc/status" 20 | ) 21 | 22 | type networkService struct { 23 | } 24 | 25 | func (s *networkService) Get(_ context.Context, req *types.NetworkGetRequest) (resp *types.NetworkGetResponse, respErr error) { 26 | defer panics.DealWith(func(recoverObj interface{}) { 27 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 28 | }) 29 | 30 | name := req.GetName() 31 | addr := req.GetAddress() 32 | index := -1 33 | if name == "" && addr == "" { 34 | ifIdx, err := getDefaultAdapterIndex() 35 | if err != nil { 36 | return nil, status.Errorf(codes.Internal, "could not get default adapter index: %v", err) 37 | } 38 | 39 | index = ifIdx 40 | } 41 | 42 | // find out how big our buffer needs to be 43 | b := make([]byte, 1) 44 | ai := (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0])) 45 | ol := uint32(0) 46 | syscall.GetAdaptersInfo(ai, &ol) 47 | 48 | // start to get info 49 | b = make([]byte, 1) 50 | ai = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0])) 51 | if err := syscall.GetAdaptersInfo(ai, &ol); err != nil { 52 | return nil, status.Errorf(codes.Internal, "could not call system GetAdaptersInfo: %v", err) 53 | } 54 | 55 | // iterate to find 56 | for ; ai != nil; ai = ai.Next { 57 | if ai.Type != windows.IF_TYPE_ETHERNET_CSMACD { 58 | continue 59 | } 60 | 61 | aiDescription := converters.UnsafeUTF16BytesToString(ai.Description[:]) 62 | aiIndex := int(ai.Index) 63 | 64 | var aiAddress, aiMask string 65 | for ipl := &ai.IpAddressList; ipl != nil; ipl = ipl.Next { 66 | aiAddress = converters.UnsafeUTF16BytesToString(ipl.IpAddress.String[:]) 67 | aiMask = converters.UnsafeUTF16BytesToString(ipl.IpMask.String[:]) 68 | if aiAddress != "" && aiMask != "" { 69 | break 70 | } 71 | } 72 | 73 | var aiGatewayAddress string 74 | for gwl := &ai.GatewayList; gwl != nil; gwl = gwl.Next { 75 | aiGatewayAddress = converters.UnsafeUTF16BytesToString(gwl.IpAddress.String[:]) 76 | if aiGatewayAddress != "" { 77 | break 78 | } 79 | } 80 | 81 | if addr == aiAddress || name == aiDescription || index == aiIndex { 82 | hostname, err := os.Hostname() 83 | if err != nil { 84 | return nil, status.Errorf(codes.Internal, "could not get system hostname: %v", err) 85 | } 86 | 87 | return &types.NetworkGetResponse{ 88 | Data: nativeToNetworkAdatper(aiIndex, aiGatewayAddress, aiAddress, aiMask, hostname), 89 | }, nil 90 | } 91 | } 92 | 93 | return nil, status.Errorf(codes.NotFound, "could not get adapter") 94 | } 95 | 96 | func nativeToNetworkAdatper(idx int, gw string, address string, mask string, hn string) *types.NetworkAdapter { 97 | addressIPNet := &net.IPNet{ 98 | IP: net.ParseIP(address), 99 | Mask: net.IPv4Mask(0xff, 0xff, 0xff, 0xff), 100 | } 101 | 102 | subnetAddressIPNet := &net.IPNet{ 103 | IP: net.ParseIP(address), 104 | Mask: make(net.IPMask, net.IPv4len), 105 | } 106 | for i, mask := range strings.SplitN(mask, ".", 4) { 107 | aInt, _ := strconv.Atoi(mask) 108 | aIntByte := byte(aInt) 109 | subnetAddressIPNet.IP[12+i] &= aIntByte 110 | subnetAddressIPNet.Mask[i] = aIntByte 111 | } 112 | 113 | return &types.NetworkAdapter{ 114 | InterfaceIndex: strconv.Itoa(idx), 115 | GatewayAddress: gw, 116 | HostName: hn, 117 | AddressCIDR: addressIPNet.String(), 118 | SubnetCIDR: subnetAddressIPNet.String(), 119 | } 120 | } 121 | 122 | func getDefaultAdapterIndex() (int, error) { 123 | // find out how big our buffer needs to be 124 | b := make([]byte, 1) 125 | ft := (*syscalls.IPForwardTable)(unsafe.Pointer(&b[0])) 126 | ol := uint32(0) 127 | syscalls.GetIPForwardTable(ft, &ol, false) 128 | 129 | // start to get table 130 | b = make([]byte, ol) 131 | ft = (*syscalls.IPForwardTable)(unsafe.Pointer(&b[0])) 132 | if err := syscalls.GetIPForwardTable(ft, &ol, false); err != nil { 133 | return -1, err 134 | } 135 | 136 | // iterate to find 137 | for i := 0; i < int(ft.NumEntries); i++ { 138 | row := *(*syscalls.IPForwardRow)(unsafe.Pointer( 139 | uintptr(unsafe.Pointer(&ft.Table[0])) + uintptr(i)*uintptr(unsafe.Sizeof(ft.Table[0])), // head idx + offset 140 | )) 141 | 142 | if converters.InetNtoa(row.ForwardDest, false) != "0.0.0.0" { 143 | continue 144 | } 145 | 146 | return int(row.ForwardIfIndex), nil 147 | } 148 | 149 | return -1, errors.New("there isn't a default gateway with a destination of 0.0.0.0") 150 | } 151 | -------------------------------------------------------------------------------- /pkg/apis/process_service.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/rancher/wins/pkg/panics" 12 | "github.com/rancher/wins/pkg/paths" 13 | "github.com/rancher/wins/pkg/types" 14 | "golang.org/x/sync/errgroup" 15 | "google.golang.org/grpc/codes" 16 | "google.golang.org/grpc/status" 17 | ) 18 | 19 | const ( 20 | processPrefix = "rancher-wins-" 21 | ) 22 | 23 | type processService struct { 24 | } 25 | 26 | func (s *processService) Start(ctx context.Context, req *types.ProcessStartRequest) (resp *types.ProcessStartResponse, respErr error) { 27 | defer panics.DealWith(func(recoverObj interface{}) { 28 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 29 | }) 30 | 31 | // ensure target bin & checksum 32 | binaryPath := filepath.Clean(req.GetPath()) 33 | if err := paths.EnsureBinary(binaryPath, req.GetChecksum()); err != nil { 34 | return nil, status.Errorf(codes.NotFound, "could not found binary: %v", err) 35 | } 36 | 37 | // could not change the name of process in windows by default, a trick way is to rename the execution binary with a special prefix 38 | binaryPathRN := renameBinary(binaryPath) 39 | if err := paths.MoveFile(binaryPath, binaryPathRN); err != nil { 40 | return nil, status.Errorf(codes.Internal, "could not rename binary: %v", err) 41 | } 42 | 43 | // create process 44 | p, err := s.create(ctx, binaryPathRN, req.GetDir(), req.GetArgs(), req.GetEnvs(), toFirewallRules(req.GetExposes())) 45 | if err != nil { 46 | return nil, status.Errorf(codes.Internal, "could not create process: %v", err) 47 | } 48 | 49 | return &types.ProcessStartResponse{ 50 | Data: &types.ProcessName{Value: p.name}, 51 | }, nil 52 | } 53 | 54 | func renameBinary(srcPath string) string { 55 | return filepath.Join(filepath.Dir(srcPath), processPrefix+filepath.Base(srcPath)) 56 | } 57 | 58 | func toFirewallRules(exposes []*types.ProcessExpose) string { 59 | exposePair := make([]string, 0, len(exposes)) 60 | 61 | for _, expose := range exposes { 62 | if expose.GetPort() != 0 { 63 | exposePair = append(exposePair, fmt.Sprintf("%s-%d", expose.GetProtocol().String(), expose.GetPort())) 64 | } 65 | } 66 | 67 | return strings.Join(exposePair, " ") 68 | } 69 | 70 | func (s *processService) Wait(req *types.ProcessWaitRequest, stream types.ProcessService_WaitServer) (respErr error) { 71 | defer panics.DealWith(func(recoverObj interface{}) { 72 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 73 | }) 74 | 75 | pname := req.GetData().GetValue() 76 | 77 | p, err := s.getFromPool(pname) 78 | if err != nil { 79 | return status.Errorf(codes.Internal, "could not get process %s: %v", pname, err) 80 | } 81 | if p == nil { 82 | return status.Errorf(codes.NotFound, "could not find process %s", pname) 83 | } 84 | 85 | errg := &errgroup.Group{} 86 | errg.Go(func() error { 87 | defer panics.Log() 88 | defer p.stdout.Close() 89 | 90 | bs := make([]byte, 1<<10) 91 | for { 92 | readSize, err := p.stdout.Read(bs) 93 | if err != nil { 94 | if io.EOF != err && io.ErrClosedPipe != err { 95 | return err 96 | } 97 | break 98 | } 99 | 100 | if readSize > 0 { 101 | err = stream.Send(&types.ProcessWaitResponse{ 102 | Options: &types.ProcessWaitResponse_StdOut{ 103 | StdOut: bs[:readSize], 104 | }, 105 | }) 106 | if err != nil { 107 | return err 108 | } 109 | } 110 | } 111 | 112 | return nil 113 | }) 114 | errg.Go(func() error { 115 | defer panics.Log() 116 | defer p.stderr.Close() 117 | 118 | bs := make([]byte, 1<<10) 119 | for { 120 | readSize, err := p.stderr.Read(bs) 121 | if err != nil { 122 | if io.EOF != err && io.ErrClosedPipe != err { 123 | return err 124 | } 125 | break 126 | } 127 | 128 | if readSize > 0 { 129 | err = stream.Send(&types.ProcessWaitResponse{ 130 | Options: &types.ProcessWaitResponse_StdErr{ 131 | StdErr: bs[:readSize], 132 | }, 133 | }) 134 | if err != nil { 135 | return err 136 | } 137 | } 138 | } 139 | 140 | return nil 141 | }) 142 | errg.Go(func() error { 143 | defer panics.Log() 144 | 145 | return p.wait() 146 | }) 147 | if err := errg.Wait(); err != nil { 148 | return status.Errorf(codes.Internal, "could not wait process %s: %v", pname, err) 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (s *processService) KeepAlive(stream types.ProcessService_KeepAliveServer) (respErr error) { 155 | defer panics.DealWith(func(recoverObj interface{}) { 156 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 157 | }) 158 | 159 | var pname string 160 | for { 161 | req, err := stream.Recv() 162 | if err != nil { 163 | break 164 | } 165 | 166 | pname = req.GetData().GetValue() 167 | } 168 | 169 | if pname == "" { 170 | return status.Errorf(codes.InvalidArgument, "could not find process with a blank string %s", pname) 171 | } 172 | 173 | p, err := s.getFromPool(pname) 174 | if err != nil { 175 | return status.Errorf(codes.Internal, "could not get process %s: %v", pname, err) 176 | } 177 | if p != nil { 178 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 179 | defer cancel() 180 | 181 | err = p.kill(ctx) 182 | if err != nil { 183 | return status.Errorf(codes.Internal, "could not kill process %s: %v", pname, err) 184 | } 185 | } 186 | 187 | stream.SendAndClose(&types.Void{}) 188 | return nil 189 | } 190 | -------------------------------------------------------------------------------- /pkg/apis/process_service_mgmt.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "syscall" 14 | "unsafe" 15 | 16 | "github.com/pkg/errors" 17 | "github.com/rancher/wins/pkg/powershell" 18 | "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var ppool sync.Map 22 | 23 | type process struct { 24 | id int 25 | name string 26 | stdout io.ReadCloser 27 | stderr io.ReadCloser 28 | } 29 | 30 | func (p *process) String() string { 31 | return fmt.Sprintf("%s(%d)", p.name, p.id) 32 | } 33 | 34 | func (p *process) wait() error { 35 | if p == nil { 36 | return errors.New("nil process") 37 | } 38 | 39 | proc, err := os.FindProcess(p.id) 40 | if err != nil { 41 | return errors.Wrapf(err, "could not find process %s", p) 42 | } 43 | 44 | _, err = proc.Wait() 45 | return err 46 | } 47 | 48 | func (p *process) kill(ctx context.Context) error { 49 | if p == nil { 50 | return errors.New("nil process") 51 | } 52 | 53 | // remove firewall route 54 | _, err := powershell.RunCommandf(`Get-NetFirewallRule -PolicyStore ActiveStore -Name %s-* | ForEach-Object {Remove-NetFirewallRule -Name $_.Name -PolicyStore ActiveStore -ErrorAction Ignore | Out-Null}`, p.name) 55 | if err != nil { 56 | return errors.Wrap(err, "could not remove firewall rules") 57 | } 58 | 59 | // kill task 60 | taskkill := exec.CommandContext(ctx, "taskkill", "/T", "/F", "/PID", strconv.Itoa(p.id)) 61 | taskkill.Run() 62 | 63 | logrus.Debugf("[Process] Killed process %s", p) 64 | ppool.Delete(p.name) 65 | 66 | return nil 67 | } 68 | 69 | func (s *processService) getFromPool(pname string) (*process, error) { 70 | po, exist := ppool.Load(pname) 71 | if exist { 72 | ret, ok := po.(*process) 73 | if ok { 74 | logrus.Debugf("[Process] Got pooled process %s", ret) 75 | return ret, nil 76 | } 77 | 78 | ppool.Delete(pname) 79 | } 80 | 81 | return nil, errors.Errorf("could not find process %s", pname) 82 | } 83 | 84 | func (s *processService) getFromHost(pname string) (*process, error) { 85 | // take windows processes snapshot 86 | snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) 87 | if err != nil { 88 | return nil, errors.Wrap(err, "could not take a snapshot for Windows processes") 89 | } 90 | defer syscall.CloseHandle(snapshot) 91 | 92 | // start the iterator 93 | var procEntry syscall.ProcessEntry32 94 | procEntry.Size = uint32(unsafe.Sizeof(procEntry)) 95 | if err := syscall.Process32First(snapshot, &procEntry); err != nil { 96 | return nil, errors.Wrap(err, "could not start the iterator for Windows processes") 97 | } 98 | 99 | // process iterating 100 | logrus.Debug("[Process] Iterating process:") 101 | var pid int 102 | for err == nil { 103 | name := getProcessName(syscall.UTF16ToString(procEntry.ExeFile[:])) 104 | if pname == name { 105 | logrus.Debugf("[Process] \tname: %s\t\tpid: %d\tppid: %d", pname, procEntry.ProcessID, procEntry.ParentProcessID) 106 | if pid == 0 || pid != int(procEntry.ParentProcessID) { 107 | pid = int(procEntry.ProcessID) 108 | } 109 | } 110 | 111 | err = syscall.Process32Next(snapshot, &procEntry) 112 | } 113 | if pid == 0 { 114 | return nil, nil 115 | } 116 | 117 | p := &process{ 118 | id: pid, 119 | name: pname, 120 | } 121 | 122 | logrus.Debugf("[Process] Got existing process %s", p) 123 | 124 | return p, nil 125 | } 126 | 127 | func (s *processService) create(ctx context.Context, path string, dir string, args []string, envs []string, fwrules string) (*process, error) { 128 | pname := getProcessName(path) 129 | 130 | pInHost, err := s.getFromHost(pname) 131 | if err != nil { 132 | return nil, err 133 | } 134 | if pInHost != nil { 135 | // detect the process is running via wins 136 | if pInPool, _ := s.getFromPool(pname); pInPool != nil { 137 | return nil, errors.Wrap(err, "could not run duplicate process") 138 | } 139 | 140 | // recreate the process to gain the std handler 141 | logrus.Warnf("[Process] Found stale process %s, try to recreate a new process", pInHost) 142 | if err := pInHost.kill(ctx); err != nil { 143 | return nil, errors.Wrap(err, "could not kill stale process") 144 | } 145 | } 146 | 147 | // create firewall rules if needed 148 | if fwrules != "" { 149 | _, err = powershell.RunCommandf(`"%s" -split ' ' | ForEach-Object {$ruleMd = $_ -split '-'; $ruleName = "%s-$_"; New-NetFirewallRule -Name $ruleName -DisplayName $ruleName -Action Allow -Protocol $ruleMd[0] -LocalPort $ruleMd[1] -Enabled True -PolicyStore ActiveStore -ErrorAction Ignore | Out-Null}`, fwrules, pname) 150 | if err != nil { 151 | return nil, errors.Wrap(err, "could not create process firewall rules") 152 | } 153 | } 154 | 155 | // create command 156 | c := exec.Command(path, args...) 157 | c.Dir = dir 158 | c.Env = append(os.Environ(), envs...) 159 | c.SysProcAttr = &syscall.SysProcAttr{ 160 | HideWindow: true, 161 | CreationFlags: 0x00000010, // CREATE_NEW_CONSOLE: https://docs.microsoft.com/en-us/windows/win32/procthread/process-creation-flags 162 | } 163 | stdout, err := c.StdoutPipe() 164 | if err != nil { 165 | return nil, errors.Wrap(err, "could not create process stdout stream") 166 | } 167 | stderr, err := c.StderrPipe() 168 | if err != nil { 169 | return nil, errors.Wrap(err, "could not create process stderr stream") 170 | } 171 | 172 | if err := c.Start(); err != nil { 173 | return nil, err 174 | } 175 | 176 | // pool process 177 | p := &process{ 178 | id: c.Process.Pid, 179 | name: pname, 180 | stdout: stdout, 181 | stderr: stderr, 182 | } 183 | ppool.Store(p.name, p) 184 | logrus.Debugf("[Process] Created process %s", p) 185 | 186 | return p, nil 187 | } 188 | 189 | func getProcessName(path string) string { 190 | return strings.TrimRight(filepath.Base(path), ".exe") 191 | } 192 | -------------------------------------------------------------------------------- /pkg/apis/route_service.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "unsafe" 8 | 9 | "github.com/rancher/wins/pkg/converters" 10 | "github.com/rancher/wins/pkg/panics" 11 | "github.com/rancher/wins/pkg/syscalls" 12 | "github.com/rancher/wins/pkg/types" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | type routeService struct { 18 | } 19 | 20 | func (s *routeService) Add(_ context.Context, req *types.RouteAddRequest) (resp *types.Void, respErr error) { 21 | defer panics.DealWith(func(recoverObj interface{}) { 22 | respErr = status.Errorf(codes.Unknown, "panic %v", recoverObj) 23 | }) 24 | 25 | var addrIPNs []*net.IPNet 26 | for _, addr := range req.GetAddresses() { 27 | _, ipn, err := net.ParseCIDR(addr) 28 | if err != nil { 29 | return nil, status.Errorf(codes.InvalidArgument, "could not recognize address %s: %v", addr, err) 30 | } 31 | 32 | addrIPNs = append(addrIPNs, ipn) 33 | } 34 | 35 | // find out how big our buffer needs to be 36 | b := make([]byte, 1) 37 | ft := (*syscalls.IPForwardTable)(unsafe.Pointer(&b[0])) 38 | ol := uint32(0) 39 | syscalls.GetIPForwardTable(ft, &ol, false) 40 | 41 | // start to get table 42 | b = make([]byte, ol) 43 | ft = (*syscalls.IPForwardTable)(unsafe.Pointer(&b[0])) 44 | if err := syscalls.GetIPForwardTable(ft, &ol, false); err != nil { 45 | return nil, status.Errorf(codes.Internal, "could not get IP table: %v", err) 46 | } 47 | 48 | // iterate to find 49 | for i := 0; i < int(ft.NumEntries); i++ { 50 | row := *(*syscalls.IPForwardRow)(unsafe.Pointer( 51 | uintptr(unsafe.Pointer(&ft.Table[0])) + uintptr(i)*uintptr(unsafe.Sizeof(ft.Table[0])), // head idx + offset 52 | )) 53 | 54 | if converters.InetNtoa(row.ForwardDest, false) != "0.0.0.0" { 55 | continue 56 | } 57 | 58 | for _, addrIPN := range addrIPNs { 59 | ip, mask := ipnetToString(addrIPN) 60 | // clone route configuration 61 | row.ForwardDest = converters.InetAton(ip, false) 62 | row.ForwardMask = converters.InetAton(mask, false) 63 | err := syscalls.CreateIPForwardEntry(&row) 64 | if err != nil { 65 | return nil, status.Errorf(codes.Internal, "could not create IP forward entry: %v", err) 66 | } 67 | } 68 | 69 | // construct response 70 | return &types.Void{}, nil 71 | } 72 | 73 | return nil, status.Error(codes.Internal, "there isn't a default gateway with a destination of 0.0.0.0") 74 | } 75 | 76 | func ipnetToString(ipNet *net.IPNet) (addr string, mask string) { 77 | a, m := ipNet.IP, ipNet.Mask 78 | return a.String(), fmt.Sprintf("%d.%d.%d.%d", m[0], m[1], m[2], m[3]) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/certs/certs.go: -------------------------------------------------------------------------------- 1 | package certs 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "io/ioutil" 10 | "math" 11 | "math/big" 12 | "time" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func GeneratePrivateKey() (*rsa.PrivateKey, error) { 18 | return rsa.GenerateKey(rand.Reader, 2048) 19 | } 20 | 21 | func GenerateSelfSignedCACert(commonName string, key *rsa.PrivateKey) (*x509.Certificate, error) { 22 | now := time.Now() 23 | tmpl := x509.Certificate{ 24 | SerialNumber: new(big.Int).SetInt64(0), 25 | Subject: pkix.Name{ 26 | CommonName: commonName, 27 | }, 28 | NotBefore: now.UTC(), 29 | NotAfter: now.Add(time.Hour * 24 * 365 * 10).UTC(), // 10 years 30 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 31 | BasicConstraintsValid: true, 32 | IsCA: true, 33 | } 34 | 35 | certDERBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, key.Public(), key) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return x509.ParseCertificate(certDERBytes) 40 | } 41 | 42 | func GenerateSignedCert(commonName string, extKeyUsages []x509.ExtKeyUsage, key *rsa.PrivateKey, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, error) { 43 | serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if len(commonName) == 0 { 48 | return nil, errors.New("must specify CommonName") 49 | } 50 | if len(extKeyUsages) == 0 { 51 | return nil, errors.New("must specify at least one ExtKeyUsage") 52 | } 53 | 54 | certTmpl := x509.Certificate{ 55 | Subject: pkix.Name{ 56 | CommonName: commonName, 57 | }, 58 | SerialNumber: serial, 59 | NotBefore: caCert.NotBefore, 60 | NotAfter: time.Now().Add(time.Hour * 24 * 365 * 10).UTC(), // 10 years 61 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 62 | ExtKeyUsage: extKeyUsages, 63 | } 64 | certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, key.Public(), caKey) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return x509.ParseCertificate(certDERBytes) 69 | } 70 | 71 | func GenerateSelfSignedCACertAndKey(commonName string) (*x509.Certificate, *rsa.PrivateKey, error) { 72 | key, err := GeneratePrivateKey() 73 | if err != nil { 74 | return nil, nil, errors.Wrap(err, "failed to generate private key") 75 | } 76 | 77 | cert, err := GenerateSelfSignedCACert(commonName, key) 78 | if err != nil { 79 | return nil, nil, errors.Wrap(err, "failed to generate cert") 80 | } 81 | 82 | return cert, key, nil 83 | } 84 | 85 | func GenerateCertAndKey(commonName string, extKeyUsages []x509.ExtKeyUsage, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) { 86 | key, err := GeneratePrivateKey() 87 | if err != nil { 88 | return nil, nil, errors.Wrap(err, "failed to generate private key") 89 | } 90 | 91 | cert, err := GenerateSignedCert(commonName, extKeyUsages, key, caCert, caKey) 92 | if err != nil { 93 | return nil, nil, errors.Wrap(err, "failed to generate cert") 94 | } 95 | 96 | return cert, key, nil 97 | } 98 | 99 | func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte { 100 | block := pem.Block{ 101 | Type: "RSA PRIVATE KEY", 102 | Bytes: x509.MarshalPKCS1PrivateKey(key), 103 | } 104 | return pem.EncodeToMemory(&block) 105 | } 106 | 107 | func EncodeCertPEM(cert *x509.Certificate) []byte { 108 | block := pem.Block{ 109 | Type: "CERTIFICATE", 110 | Bytes: cert.Raw, 111 | } 112 | return pem.EncodeToMemory(&block) 113 | } 114 | 115 | func WritePrivateKeyPEM(path string, key *rsa.PrivateKey) error { 116 | return ioutil.WriteFile(path, EncodePrivateKeyPEM(key), 0600) 117 | } 118 | 119 | func WriteCertPEM(path string, cert *x509.Certificate) error { 120 | return ioutil.WriteFile(path, EncodeCertPEM(cert), 0600) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/concierge/concierge.go: -------------------------------------------------------------------------------- 1 | package concierge 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/sirupsen/logrus" 8 | "golang.org/x/sys/windows" 9 | "golang.org/x/sys/windows/registry" 10 | "golang.org/x/sys/windows/svc" 11 | "golang.org/x/sys/windows/svc/mgr" 12 | ) 13 | 14 | type Config struct { 15 | Args []string 16 | Description string 17 | DisplayName string 18 | EnvVars []string 19 | registryKey string 20 | } 21 | 22 | type Concierge struct { 23 | name string 24 | path string 25 | cfg *Config 26 | } 27 | 28 | // New creates a new Concierge for managing a Windows Service. 29 | func New(name, path string, cfg *Config) (*Concierge, error) { 30 | if name == "" { 31 | return nil, errors.New("name isn't set and can't be empty") 32 | } 33 | 34 | if path == "" { 35 | return nil, errors.New("path isn't set and can't be empty") 36 | } 37 | if cfg == nil { 38 | return nil, errors.New("cfg is nil, please provide at least an empty config") 39 | } 40 | 41 | cfg.registryKey = fmt.Sprintf(`SYSTEM\CurrentControlSet\Services\%s`, name) 42 | 43 | return &Concierge{ 44 | name: name, 45 | path: path, 46 | cfg: cfg, 47 | }, nil 48 | } 49 | 50 | // Enable will start the Windows Service. If the service doesn't exist it will create it. 51 | func (c *Concierge) Enable() error { 52 | var service *mgr.Service 53 | ok, err := c.ServiceExists() 54 | if err != nil { 55 | return errors.Wrap(err, "error checking if the service exists") 56 | } 57 | if !ok { 58 | if err = c.CreateService(); err != nil { 59 | return errors.Wrap(err, "error creating the service") 60 | } 61 | } 62 | 63 | if service, err = c.fetchService(); err != nil { 64 | return errors.Wrap(err, "error fetching the service") 65 | } 66 | defer service.Close() 67 | 68 | return service.Start() 69 | } 70 | 71 | // Disable will stop the Windows Service. 72 | func (c *Concierge) Disable() error { 73 | var service *mgr.Service 74 | ok, err := c.ServiceExists() 75 | if err != nil { 76 | return errors.Wrap(err, "error checking if the service exists") 77 | } 78 | if !ok { 79 | return errors.Errorf("service %s not found", c.path) 80 | } 81 | 82 | if service, err = c.fetchService(); err != nil { 83 | return errors.Wrap(err, "error fetching the service") 84 | } 85 | defer service.Close() 86 | 87 | if _, err := service.Control(svc.Stop); err != nil { 88 | return errors.Wrap(err, "error stopping the service") 89 | } 90 | return nil 91 | } 92 | 93 | // CreateService configures the Windows service correctly, returning the service. 94 | func (c *Concierge) CreateService() error { 95 | m, err := mgr.Connect() 96 | if err != nil { 97 | return errors.Wrap(err, "could not open SCM") 98 | } 99 | defer m.Disconnect() 100 | 101 | service, err := m.CreateService(c.name, c.path, mgr.Config{ 102 | ServiceType: windows.SERVICE_WIN32_OWN_PROCESS, 103 | StartType: mgr.StartAutomatic, 104 | ErrorControl: mgr.ErrorNormal, 105 | BinaryPathName: c.path, 106 | Description: c.cfg.Description, 107 | DisplayName: c.cfg.DisplayName, 108 | }, c.cfg.Args...) 109 | 110 | defer service.Close() 111 | 112 | if err != nil { 113 | return errors.Wrap(err, "error creating the service") 114 | } 115 | 116 | recoveryActions := []mgr.RecoveryAction{ 117 | { 118 | Type: mgr.ServiceRestart, 119 | Delay: 10000, 120 | }, 121 | } 122 | if err := c.registerEnvVars(); err != nil { 123 | return err 124 | } 125 | 126 | return service.SetRecoveryActions(recoveryActions, 0) 127 | } 128 | 129 | // Delete removes the service and any registry keys. 130 | func (c *Concierge) Delete() error { 131 | var service *mgr.Service 132 | ok, err := c.ServiceExists() 133 | if err != nil { 134 | return errors.Wrap(err, "error checking if the service exists") 135 | } 136 | if !ok { 137 | return errors.Errorf("service %s not found", c.path) 138 | } 139 | 140 | if service, err = c.fetchService(); err != nil { 141 | return errors.Wrap(err, "error fetching the service") 142 | } 143 | defer service.Close() 144 | 145 | return service.Delete() 146 | } 147 | 148 | // ServiceExists retrieves the Windows service if exists. 149 | func (c *Concierge) ServiceExists() (bool, error) { 150 | m, err := mgr.Connect() 151 | if err != nil { 152 | return false, errors.Wrap(err, "could not open SCM") 153 | } 154 | defer func(m *mgr.Mgr) { 155 | _ = m.Disconnect() 156 | }(m) 157 | 158 | services, err := m.ListServices() 159 | if err != nil { 160 | return false, errors.Wrap(err, "could not list services") 161 | } 162 | 163 | for _, service := range services { 164 | if service == c.name { 165 | return true, nil 166 | } 167 | } 168 | 169 | return false, nil 170 | } 171 | 172 | // State gets the state of the service. Examples are stopped, running, etc. 173 | func (c *Concierge) State() (svc.State, error) { 174 | service, err := c.fetchService() 175 | if err != nil { 176 | return svc.State(windows.SERVICE_NO_CHANGE), errors.Wrap(err, "error opening the service") 177 | } 178 | defer service.Close() 179 | 180 | status, err := service.Query() 181 | if err != nil { 182 | return svc.State(windows.SERVICE_NO_CHANGE), errors.Wrap(err, "error querying the service") 183 | } 184 | return status.State, nil 185 | } 186 | 187 | // fetchService retrieves the Windows service. 188 | func (c *Concierge) fetchService() (*mgr.Service, error) { 189 | m, err := mgr.Connect() 190 | if err != nil { 191 | return nil, errors.Wrap(err, "could not open SCM") 192 | } 193 | defer m.Disconnect() 194 | 195 | service, err := m.OpenService(c.name) 196 | if err != nil { 197 | return nil, errors.Wrap(err, "could not open service") 198 | } 199 | 200 | return service, nil 201 | } 202 | 203 | // registerEnvVars creates a registry key for the service to set environment variables. 204 | func (c *Concierge) registerEnvVars() error { 205 | if len(c.cfg.EnvVars) == 0 { 206 | logrus.Infof("skipping environment variable configuration for %s, none are provided", c.name) 207 | return nil 208 | } 209 | 210 | k, err := registry.OpenKey(registry.LOCAL_MACHINE, c.cfg.registryKey, registry.WRITE) 211 | if err != nil { 212 | return errors.Wrap(err, "error opening registry key") 213 | } 214 | defer k.Close() 215 | 216 | return k.SetStringsValue("Environment", c.cfg.EnvVars) 217 | } 218 | -------------------------------------------------------------------------------- /pkg/converters/binary.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | ) 7 | 8 | func InetNtoa(ipnr uint32, isBig bool) string { 9 | ip := net.IPv4(0, 0, 0, 0) 10 | var bo binary.ByteOrder 11 | if isBig { 12 | bo = binary.BigEndian 13 | } else { 14 | bo = binary.LittleEndian 15 | } 16 | bo.PutUint32([]byte(ip.To4()), ipnr) 17 | return ip.String() 18 | } 19 | 20 | func InetAton(ip string, isBig bool) uint32 { 21 | var bo binary.ByteOrder 22 | if isBig { 23 | bo = binary.BigEndian 24 | } else { 25 | bo = binary.LittleEndian 26 | } 27 | return bo.Uint32( 28 | []byte(net.ParseIP(ip).To4()), 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/converters/byte.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "unsafe" 7 | ) 8 | 9 | func UnsafeBytesToString(bs []byte) string { 10 | return *(*string)(unsafe.Pointer(&bs)) 11 | } 12 | 13 | func UnsafeStringToBytes(str string) []byte { 14 | return *(*[]byte)(unsafe.Pointer((*reflect.SliceHeader)(unsafe.Pointer(&str)))) 15 | } 16 | 17 | func UnsafeUTF16BytesToString(bs []byte) string { 18 | return UnsafeBytesToString(bytes.Trim(bs, "\x00")) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/converters/byte_test.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUnsafeStringToBytes(t *testing.T) { 8 | want := "hello world" 9 | got := string(UnsafeStringToBytes("hello world")) 10 | if got != want { 11 | t.Errorf("error, should be %s, but got %s", want, got) 12 | } 13 | } 14 | 15 | func TestUnsafeBytesToString(t *testing.T) { 16 | want := "hello world" 17 | got := UnsafeBytesToString([]byte("hello world")) 18 | if got != want { 19 | t.Errorf("error, should be %s, but got %s", want, got) 20 | } 21 | } 22 | 23 | func TestUnsafeUTF16BytesToString(t *testing.T) { 24 | want := "hello" 25 | got := UnsafeUTF16BytesToString([]byte{ 26 | 0x00, 27 | 'h', 28 | 'e', 29 | 'l', 30 | 'l', 31 | 'o', 32 | 0x00}) 33 | if got != want { 34 | t.Errorf("error, should be %s, but got %s", want, got) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/converters/error_ignore.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/buger/jsonparser" 7 | "golang.org/x/sys/windows/registry" 8 | ) 9 | 10 | func GetStringFormJSON(jsonData []byte, key ...string) string { 11 | val, _ := jsonparser.GetUnsafeString(jsonData, key...) 12 | return val 13 | } 14 | 15 | func GetIntFromRegistryKey(k registry.Key, name string) int { 16 | val, _, _ := k.GetIntegerValue(name) 17 | return int(val) 18 | } 19 | 20 | func GetIntStringFormRegistryKey(k registry.Key, name string) string { 21 | return strconv.Itoa(GetIntFromRegistryKey(k, name)) 22 | } 23 | 24 | func GetStringFromRegistryKey(k registry.Key, name string) string { 25 | val, _, _ := k.GetStringValue(name) 26 | return val 27 | } 28 | -------------------------------------------------------------------------------- /pkg/converters/json.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ghodss/yaml" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func ToJSON(obj interface{}) (string, error) { 11 | ret, err := json.Marshal(obj) 12 | if err != nil { 13 | return "", errors.Wrapf(err, "could not convert %T obj to JSON", obj) 14 | } 15 | 16 | return UnsafeBytesToString(ret), nil 17 | } 18 | 19 | func ToYaml(obj interface{}) (string, error) { 20 | ret, err := yaml.Marshal(obj) 21 | if err != nil { 22 | return "", errors.Wrapf(err, "could not convert %T obj to YAML", obj) 23 | } 24 | 25 | return UnsafeBytesToString(ret), nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/converters/json_test.go: -------------------------------------------------------------------------------- 1 | package converters 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestToJson(t *testing.T) { 9 | json := struct { 10 | A string 11 | B string `json:"bAlias"` 12 | C string `json:"-"` 13 | }{ 14 | A: "a", 15 | B: "b", 16 | C: "c", 17 | } 18 | 19 | want := `{"A":"a","bAlias":"b"}` 20 | 21 | got, err := ToJSON(json) 22 | if err != nil { 23 | t.Errorf("error occurred, %v", err) 24 | } 25 | 26 | if got != want { 27 | t.Errorf("error, should be %s, but got %s", want, got) 28 | } 29 | } 30 | 31 | func TestToYaml(t *testing.T) { 32 | yaml := struct { 33 | A string 34 | B string `json:"-"` 35 | C string `json:"-"` 36 | }{ 37 | A: "a", 38 | B: "b", 39 | C: "c", 40 | } 41 | 42 | want := "A: a" 43 | 44 | got, err := ToYaml(yaml) 45 | if err != nil { 46 | t.Errorf("error occurred, %v", err) 47 | } 48 | 49 | if strings.TrimSpace(got) != want { 50 | t.Errorf("error, should be %s, but got %s", want, got) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/csiproxy/csi.go: -------------------------------------------------------------------------------- 1 | package csiproxy 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "crypto/tls" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/sirupsen/logrus" 15 | 16 | "github.com/pkg/errors" 17 | "github.com/rancher/wins/pkg/concierge" 18 | winstls "github.com/rancher/wins/pkg/tls" 19 | ) 20 | 21 | const ( 22 | exeName = "csi-proxy.exe" 23 | serviceName = "csiproxy" 24 | ) 25 | 26 | // Config is the CSI Proxy config settings 27 | type Config struct { 28 | URL string `yaml:"url" json:"url"` 29 | Version string `yaml:"version" json:"version"` 30 | KubeletPath string `yaml:"kubeletPath" json:"kubeletPath"` 31 | } 32 | 33 | // Validate ensures that the configuration for CSI Proxy is correct if provided. 34 | func (c *Config) validate() error { 35 | if strings.TrimSpace(c.URL) == "" { 36 | return errors.New("CSI Proxy URL cannot be empty") 37 | } 38 | 39 | if strings.TrimSpace(c.Version) == "" { 40 | return errors.New("CSI Proxy version cannot be empty") 41 | } 42 | 43 | if strings.TrimSpace(c.KubeletPath) == "" { 44 | return errors.New("kubelet path cannot be empty") 45 | } 46 | return nil 47 | } 48 | 49 | // Proxy is for creating and retrieving the Windows Service 50 | type Proxy struct { 51 | cfg *Config 52 | tlsCfg *winstls.Config 53 | serviceName string 54 | binaryName string 55 | binaryPath string 56 | concierge *concierge.Concierge 57 | } 58 | 59 | // New creates a new Proxy struct 60 | func New(cfg *Config, tlsCfg *winstls.Config) (*Proxy, error) { 61 | if err := cfg.validate(); err != nil { 62 | return nil, err 63 | } 64 | 65 | cwd, err := os.Getwd() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | config := concierge.Config{ 71 | Args: []string{"-windows-service", "-log_file=\\etc\\rancher\\wins\\csi-proxy.log", "-logtostderr=false"}, 72 | Description: "Manages the Kubernetes CSI Proxy application.", 73 | DisplayName: "CSI Proxy", 74 | EnvVars: nil, 75 | } 76 | 77 | service, err := concierge.New(serviceName, filepath.Join(cwd, exeName), &config) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return &Proxy{ 83 | cfg: cfg, 84 | tlsCfg: tlsCfg, 85 | serviceName: serviceName, 86 | binaryName: exeName, 87 | binaryPath: filepath.Join(cwd, exeName), 88 | concierge: service, 89 | }, nil 90 | } 91 | 92 | func (p *Proxy) Enable() error { 93 | ok, err := p.concierge.ServiceExists() 94 | if err != nil { 95 | return err 96 | } 97 | if !ok { 98 | if p.tlsCfg != nil && p.tlsCfg.CertFilePath != "" { 99 | // CSI Proxy does not need the certpool that is returned 100 | _, err := p.tlsCfg.SetupGenericTLSConfigFromFile() 101 | if err != nil { 102 | 103 | return err 104 | } 105 | } 106 | logrus.Infof("CSI Proxy is being downloaded.") 107 | if err := p.download(); err != nil { 108 | return err 109 | } 110 | logrus.Infof("CSI Proxy is being started.") 111 | if err := p.concierge.Enable(); err != nil { 112 | return err 113 | } 114 | } 115 | return nil 116 | } 117 | 118 | // download retrieves the CSI Proxy executable from the config settings. 119 | func (p *Proxy) download() error { 120 | file, err := os.Create(p.binaryPath) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | defer func(file *os.File) { 126 | _ = file.Close() 127 | }(file) 128 | 129 | client := http.Client{ 130 | CheckRedirect: func(r *http.Request, _ []*http.Request) error { 131 | r.URL.Opaque = r.URL.Path 132 | return nil 133 | }, 134 | } 135 | 136 | // default to insecure which matches system-agent functionality 137 | // if a proxy is set with the proper envvars, we will use it 138 | // as long as the req does not match an entry in no_proxy env var 139 | transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, Proxy: http.ProxyFromEnvironment} 140 | 141 | if p.tlsCfg != nil && !*p.tlsCfg.Insecure && p.tlsCfg.CertFilePath != "" { 142 | transport.TLSClientConfig.InsecureSkipVerify = false 143 | } 144 | 145 | client.Transport = transport 146 | 147 | defer client.CloseIdleConnections() 148 | 149 | resp, err := client.Get(fmt.Sprintf(p.cfg.URL, p.cfg.Version)) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | defer func(Body io.ReadCloser) { 155 | _ = Body.Close() 156 | }(resp.Body) 157 | 158 | gz, err := gzip.NewReader(resp.Body) 159 | if err != nil { 160 | return err 161 | } 162 | defer func(gz *gzip.Reader) { 163 | _ = gz.Close() 164 | }(gz) 165 | 166 | tr := tar.NewReader(gz) 167 | for { 168 | hdr, err := tr.Next() 169 | 170 | if err == io.EOF { 171 | break 172 | } 173 | if err != nil { 174 | return err 175 | } 176 | 177 | if strings.Contains(hdr.Name, p.binaryName) { 178 | if _, err := io.Copy(file, tr); err != nil { 179 | return err 180 | } 181 | } 182 | } 183 | return nil 184 | } 185 | -------------------------------------------------------------------------------- /pkg/defaults/constant.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | const ( 4 | PermissionBuiltinAdministratorsAndLocalSystem = "D:P(A;;GA;;;BA)(A;;GA;;;SY)" 5 | WindowsServiceName = "rancher-wins" 6 | WindowsProcessName = "wins" 7 | WindowsSUCName = "rancher-wins-suc" 8 | WindowsServiceDisplayName = "Rancher Wins" 9 | NamedPipeName = "rancher_wins" 10 | ProxyPipeName = "rancher_wins_proxy" 11 | ) 12 | -------------------------------------------------------------------------------- /pkg/defaults/variable.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | var ( 8 | AppVersion = "dev" 9 | AppCommit = "0000000" 10 | ConfigPath = filepath.Join("c:/", "etc", "rancher", "wins", "config") 11 | CertPath = filepath.Join("c:/", "etc", "rancher", "agent", "ranchercert") 12 | ) 13 | -------------------------------------------------------------------------------- /pkg/logs/etw.go: -------------------------------------------------------------------------------- 1 | package logs 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/Microsoft/go-winio/pkg/etw" 9 | "github.com/Microsoft/go-winio/pkg/etwlogrus" 10 | "github.com/Microsoft/go-winio/pkg/guid" 11 | "github.com/pkg/errors" 12 | "github.com/rancher/wins/pkg/profilings" 13 | "github.com/sirupsen/logrus" 14 | "golang.org/x/sys/windows" 15 | "golang.org/x/sys/windows/svc/eventlog" 16 | ) 17 | 18 | const ( 19 | // These should match the values in event_messages.mc. 20 | eventInfo = 1 21 | eventWarn = 1 22 | eventError = 1 23 | eventDebug = 2 24 | eventPanic = 3 25 | eventFatal = 4 26 | 27 | eventExtraOffset = 10 // Add this to any event to get a string that supports extended data 28 | ) 29 | 30 | type eventLogHook struct { 31 | log *eventlog.Log 32 | } 33 | 34 | func (h *eventLogHook) Levels() []logrus.Level { 35 | return logrus.AllLevels 36 | } 37 | 38 | func (h *eventLogHook) Fire(e *logrus.Entry) error { 39 | var ( 40 | etype uint16 41 | eid uint32 42 | ) 43 | 44 | switch e.Level { 45 | case logrus.PanicLevel: 46 | etype = windows.EVENTLOG_ERROR_TYPE 47 | eid = eventPanic 48 | case logrus.FatalLevel: 49 | etype = windows.EVENTLOG_ERROR_TYPE 50 | eid = eventFatal 51 | case logrus.ErrorLevel: 52 | etype = windows.EVENTLOG_ERROR_TYPE 53 | eid = eventError 54 | case logrus.WarnLevel: 55 | etype = windows.EVENTLOG_WARNING_TYPE 56 | eid = eventWarn 57 | case logrus.InfoLevel: 58 | etype = windows.EVENTLOG_INFORMATION_TYPE 59 | eid = eventInfo 60 | case logrus.DebugLevel: 61 | etype = windows.EVENTLOG_INFORMATION_TYPE 62 | eid = eventDebug 63 | default: 64 | return errors.New("unknown level") 65 | } 66 | 67 | // If there is additional data, include it as a second string. 68 | exts := "" 69 | if len(e.Data) > 0 { 70 | fs := bytes.Buffer{} 71 | for k, v := range e.Data { 72 | fs.WriteString(k) 73 | fs.WriteByte('=') 74 | fmt.Fprint(&fs, v) 75 | fs.WriteByte(' ') 76 | } 77 | 78 | exts = fs.String()[:fs.Len()-1] 79 | eid += eventExtraOffset 80 | } 81 | 82 | if h.log == nil { 83 | fmt.Fprintf(os.Stderr, "%s [%s]\n", e.Message, exts) 84 | return nil 85 | } 86 | 87 | var ( 88 | ss [2]*uint16 89 | err error 90 | ) 91 | 92 | ss[0], err = windows.UTF16PtrFromString(e.Message) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | count := uint16(1) 98 | if exts != "" { 99 | ss[1], err = windows.UTF16PtrFromString(exts) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | count++ 105 | } 106 | 107 | return windows.ReportEvent(h.log.Handle, etype, 0, eid, 0, count, 0, &ss[0], nil) 108 | } 109 | 110 | func NewEventLogHook(serviceName string) (logrus.Hook, error) { 111 | elog, err := eventlog.Open(serviceName) 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | return &eventLogHook{log: elog}, nil 117 | } 118 | 119 | func etwCallback(_ guid.GUID, state etw.ProviderState, _ etw.Level, _ uint64, _ uint64, _ uintptr) { 120 | if state == etw.ProviderStateCaptureState { 121 | logrus.Infof("=== BEGIN goroutine stack dump ===\n%s\n=== END goroutine stack dump ===", profilings.DumpStacks()) 122 | } 123 | } 124 | 125 | func NewEtwProviderHook(serviceName string) (logrus.Hook, error) { 126 | p, err := etw.NewProvider(serviceName, etwCallback) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | return etwlogrus.NewHookFromProvider(p) 132 | } 133 | -------------------------------------------------------------------------------- /pkg/npipes/npipe.go: -------------------------------------------------------------------------------- 1 | package npipes 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "github.com/Microsoft/go-winio" 11 | "github.com/pkg/errors" 12 | "github.com/rancher/wins/pkg/defaults" 13 | ) 14 | 15 | type Dialer func(context.Context, string) (net.Conn, error) 16 | 17 | // NewDialer creates a Dialer to connect to a named pipe by `path`. 18 | func NewDialer(path string, timeout time.Duration) (Dialer, error) { 19 | path, err := ParsePath(path) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return func(_ context.Context, _ string) (conn net.Conn, e error) { 25 | return winio.DialPipe(path, &timeout) 26 | }, nil 27 | } 28 | 29 | // New creates a named pipe with `path`, `sddl` and `bufferSize` 30 | // `sddl`: a format string of the Security Descriptor Definition Language, default is builtin administrators and local system 31 | // `bufferSize`: measurement is KB, default is 64 32 | // refer: 33 | // - https://docs.microsoft.com/en-us/windows/desktop/secauthz/security-descriptor-string-format 34 | // - https://docs.microsoft.com/en-us/windows/desktop/secauthz/ace-strings 35 | func New(path, sddl string, bufferSize int32) (net.Listener, error) { 36 | path, err := ParsePath(path) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if sddl == "" { 42 | // Allow Administrators and SYSTEM, plus whatever additional users or groups were specified 43 | sddl = defaults.PermissionBuiltinAdministratorsAndLocalSystem 44 | } 45 | if bufferSize == 0 { 46 | // Use 64KB buffers to improve performance 47 | bufferSize = 64 48 | } 49 | bufferSize *= int32(1 << 10) 50 | 51 | pipeConfig := winio.PipeConfig{ 52 | SecurityDescriptor: sddl, 53 | MessageMode: true, 54 | InputBufferSize: bufferSize, 55 | OutputBufferSize: bufferSize, 56 | } 57 | 58 | listener, err := winio.ListenPipe(path, &pipeConfig) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return listener, nil 64 | } 65 | 66 | // GetFullPath returns the full path with the named pipe name 67 | func GetFullPath(name string) string { 68 | return fmt.Sprintf("npipe:////./pipe/%s", name) 69 | } 70 | 71 | func ParsePath(path string) (string, error) { 72 | sps := strings.SplitN(path, "://", 2) 73 | if len(sps) != 2 { 74 | return "", errors.Errorf("could not recognize path: %s", path) 75 | } 76 | 77 | if sps[0] != "npipe" { 78 | return "", errors.Errorf("could not recognize schema: %s", sps[0]) 79 | } 80 | npipePath := sps[1] 81 | 82 | return strings.ReplaceAll(npipePath, "/", `\`), nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/npipes/npipe_test.go: -------------------------------------------------------------------------------- 1 | package npipes 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestCreatePipe(t *testing.T) { 9 | type args struct { 10 | name string 11 | sddl string 12 | bufferSize int32 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want string 18 | error bool 19 | }{ 20 | { 21 | name: "create pipe", 22 | args: args{name: "//./pipe/test", sddl: "", bufferSize: 0}, 23 | want: "could not recognize path:", 24 | error: true, 25 | }, 26 | { 27 | name: "get path to pipe", 28 | args: args{name: GetFullPath("test"), sddl: "", bufferSize: 0}, 29 | error: false, 30 | }, 31 | { 32 | name: "duplicate listener", 33 | args: args{name: "npipe:////./pipe/test", sddl: "", bufferSize: 0}, 34 | want: "Access is denied.", 35 | error: true, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | _, err := New(tt.args.name, tt.args.sddl, tt.args.bufferSize) 42 | if err != nil { 43 | if !tt.error { 44 | t.Errorf("error occurred, %v", err) 45 | } 46 | if !strings.Contains(err.Error(), tt.want) { 47 | t.Errorf("error, should be %s, but got %s", tt.want, err.Error()) 48 | } 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/panics/recover.go: -------------------------------------------------------------------------------- 1 | package panics 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | func Log() { 8 | if r := recover(); r != nil { 9 | logrus.Errorf("panic: %s", r) 10 | } 11 | } 12 | 13 | func DealWith(handler func(recoverObj interface{})) { 14 | if r := recover(); r != nil { 15 | if handler != nil { 16 | handler(r) 17 | } 18 | } 19 | } 20 | 21 | func Ignore() { 22 | // nothing to do 23 | } 24 | -------------------------------------------------------------------------------- /pkg/paths/directory.go: -------------------------------------------------------------------------------- 1 | package paths 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func IncludeFiles(path string, filenames ...string) (bool, error) { 11 | if len(filenames) == 0 { 12 | return false, errors.New("could not detect empty filename collection") 13 | } 14 | 15 | if err := EnsureDirectory(path); err != nil { 16 | return false, errors.Wrapf(err, "failed to ensure directory %s", path) 17 | } 18 | 19 | for _, f := range filenames { 20 | fpath := filepath.Join(path, f) 21 | fs, err := os.Stat(fpath) 22 | if err != nil { 23 | if os.IsNotExist(err) { 24 | return false, nil 25 | } 26 | return false, errors.Wrapf(err, "%s could not be touched", fpath) 27 | } else if fs.IsDir() { 28 | return false, errors.Errorf("%s is directory", fpath) 29 | } 30 | } 31 | 32 | return true, nil 33 | } 34 | 35 | func EnsureDirectory(dir string) error { 36 | d, err := os.Stat(dir) 37 | if err != nil { 38 | if !os.IsNotExist(err) { 39 | return err 40 | } 41 | 42 | err = os.MkdirAll(dir, os.ModePerm) 43 | if err != nil { 44 | return err 45 | } 46 | } else if !d.IsDir() { 47 | return errors.New("it's not a directory") 48 | } 49 | 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/paths/file.go: -------------------------------------------------------------------------------- 1 | package paths 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "encoding/hex" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/fsnotify/fsnotify" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | func GetFileSHA1Hash(path string) (string, error) { 16 | h := sha1.New() 17 | 18 | fs, err := os.Open(path) 19 | if err != nil { 20 | return "", err 21 | } 22 | defer fs.Close() 23 | 24 | s, err := fs.Stat() 25 | if err != nil { 26 | return "", err 27 | } 28 | if s.IsDir() { 29 | return "", errors.Errorf("%s is not a file", path) 30 | } 31 | 32 | _, err = io.Copy(h, fs) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return hex.EncodeToString(h.Sum(nil)), nil 38 | } 39 | 40 | func MoveFile(srcPath, targetPath string) error { 41 | dir := filepath.Dir(targetPath) 42 | d, err := os.Stat(dir) 43 | if err != nil { 44 | if !os.IsNotExist(err) { 45 | return errors.Wrap(err, "could not detect the directory of target file") 46 | } 47 | 48 | err = os.Mkdir(dir, os.ModePerm) 49 | if err != nil { 50 | return errors.Wrap(err, "could not create the directory of target file") 51 | } 52 | } else if !d.IsDir() { 53 | return errors.Errorf("%s is not a directory", dir) 54 | } 55 | 56 | // if target path is already existing 57 | if _, err := os.Stat(targetPath); err == nil { 58 | // it already exists so delete it so we can move the new one in 59 | if err = os.Remove(targetPath); err != nil { 60 | return errors.Wrapf(err, "could not remove existing target file %s", targetPath) 61 | } 62 | } 63 | 64 | if err = os.Rename(srcPath, targetPath); err != nil { 65 | return errors.Wrapf(err, "could not move the source file to the target %s", targetPath) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | type WatchHandle func(watchErr error, watchEvent fsnotify.Event) 72 | 73 | func Watch(ctx context.Context, path string, handle WatchHandle) error { 74 | watcher, err := fsnotify.NewWatcher() 75 | if err != nil { 76 | return errors.Wrap(err, "could not start fsnotify watcher") 77 | } 78 | 79 | go func() { 80 | defer watcher.Close() 81 | 82 | for { 83 | select { 84 | case <-ctx.Done(): 85 | return 86 | case event, ok := <-watcher.Events: 87 | if !ok { 88 | return 89 | } 90 | if handle != nil { 91 | handle(nil, event) 92 | } 93 | case err, ok := <-watcher.Errors: 94 | if !ok { 95 | return 96 | } 97 | if handle != nil { 98 | handle(err, fsnotify.Event{}) 99 | } 100 | } 101 | } 102 | }() 103 | 104 | err = watcher.Add(path) 105 | if err != nil { 106 | return errors.Wrapf(err, "could not add watching for %s", path) 107 | } 108 | 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/paths/file_binary.go: -------------------------------------------------------------------------------- 1 | package paths 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func GetBinaryPath(binaryName string) (string, error) { 12 | // find service abs path 13 | p, err := exec.LookPath(binaryName) 14 | if err != nil { 15 | return "", err 16 | } 17 | p, err = filepath.Abs(p) 18 | if err != nil { 19 | return "", err 20 | } 21 | 22 | // detect service is file or not 23 | fi, err := os.Stat(p) 24 | if err == nil { 25 | if !fi.IsDir() { 26 | return p, nil 27 | } 28 | err = errors.Errorf("%s is directory", p) 29 | } 30 | if filepath.Ext(p) == "" { 31 | p += ".exe" 32 | fi, err := os.Stat(p) 33 | if err == nil { 34 | if !fi.IsDir() { 35 | return p, nil 36 | } 37 | return "", errors.Errorf("%s is directory", p) 38 | } 39 | } 40 | 41 | return "", err 42 | } 43 | 44 | func GetBinarySHA1Hash(binaryName string) (string, error) { 45 | path, err := GetBinaryPath(binaryName) 46 | if err != nil { 47 | return "", err 48 | } 49 | actualChecksum, err := GetFileSHA1Hash(path) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | return actualChecksum, nil 55 | } 56 | 57 | func EnsureBinary(binaryName string, expectedChecksum string) error { 58 | actualChecksum, err := GetBinarySHA1Hash(binaryName) 59 | if err != nil { 60 | return errors.Wrap(err, "could not get checksum") 61 | } 62 | if expectedChecksum != actualChecksum { 63 | return errors.Errorf("could not match (expect checksum %q, but get %q)", expectedChecksum, actualChecksum) 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /pkg/powershell/powershell.go: -------------------------------------------------------------------------------- 1 | package powershell 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | // Sourced from https://github.com/flannel-io/flannel/blob/d31b0dc85a5a15bda5e606acbbbb9f7089441a87/pkg/powershell/powershell.go 12 | 13 | // commandWrapper ensures that exceptions are written to stdout and the powershell process exit code is -1 14 | const commandWrapper = `$ErrorActionPreference="Stop";try { %s } catch { Write-Host $_; os.Exit(-1) }` 15 | 16 | // RunCommand executes a given powershell command. 17 | // 18 | // When the command throws a powershell exception, RunCommand will return the exception message as error. 19 | func RunCommand(command string) ([]byte, error) { 20 | cmd := exec.Command("powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", fmt.Sprintf(commandWrapper, command)) 21 | 22 | stdout, err := cmd.Output() 23 | if err != nil { 24 | if cmd.ProcessState.ExitCode() != 0 { 25 | message := strings.TrimSpace(string(stdout)) 26 | return []byte{}, errors.New(message) 27 | } 28 | 29 | return []byte{}, err 30 | } 31 | 32 | return stdout, nil 33 | } 34 | 35 | // RunCommandf executes a given powershell command. Command argument formats according to a format specifier (See fmt.Sprintf). 36 | // 37 | // When the command throws a powershell exception, RunCommandf will return the exception message as error. 38 | func RunCommandf(command string, a ...interface{}) ([]byte, error) { 39 | return RunCommand(fmt.Sprintf(command, a...)) 40 | } 41 | 42 | // RunCommandWithJSONResult executes a given powershell command. 43 | // The command will be wrapped with ConvertTo-Json. 44 | // 45 | // You can Wrap your command with @() to ensure that the returned json is an array 46 | // 47 | // When the command throws a powershell exception, RunCommandf will return the exception message as error. 48 | func RunCommandWithJSONResult(command string, v interface{}) error { 49 | wrappedCommand := fmt.Sprintf(commandWrapper, "ConvertTo-Json (%s)") 50 | wrappedCommand = fmt.Sprintf(wrappedCommand, command) 51 | 52 | stdout, err := RunCommandf(wrappedCommand) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | return json.Unmarshal(stdout, v) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/powershell/powershell_test.go: -------------------------------------------------------------------------------- 1 | package powershell 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestExecuteCommand(t *testing.T) { 9 | type args struct { 10 | command string 11 | } 12 | 13 | tests := []struct { 14 | name string 15 | args args 16 | want string 17 | error bool 18 | }{ 19 | { 20 | name: "write to standard out", 21 | args: args{command: "Write-Output 'test-value'"}, 22 | want: "test-value", 23 | error: false, 24 | }, 25 | { 26 | name: "write to host", 27 | args: args{command: "Write-Host 'test-value'"}, 28 | want: "test-value", 29 | error: false, 30 | }, 31 | { 32 | name: "write to standard error", 33 | args: args{command: "Write-Error 'test-value'"}, 34 | want: "test-value", 35 | error: true, 36 | }, 37 | } 38 | 39 | for _, tt := range tests { 40 | t.Run(tt.name, func(t *testing.T) { 41 | o, err := RunCommand(tt.args.command) 42 | if err != nil && !tt.error { 43 | t.Errorf("error occurred, %s", err.Error()) 44 | } 45 | 46 | if !tt.error { 47 | got := strings.TrimSpace(string(o)) 48 | if got != tt.want { 49 | t.Errorf("error, should be %s, but got %s ", tt.want, got) 50 | } 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/profilings/dump.go: -------------------------------------------------------------------------------- 1 | package profilings 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "reflect" 7 | "strings" 8 | "unsafe" 9 | 10 | "github.com/rancher/wins/pkg/defaults" 11 | "github.com/sirupsen/logrus" 12 | "golang.org/x/sys/windows" 13 | 14 | "syscall" 15 | ) 16 | 17 | func StackDump() (err error) { 18 | err = callStackDump() 19 | if err != nil { 20 | logrus.Errorf("[StackDump] failed to call wins stack dump: %v", err) 21 | return err 22 | } 23 | return nil 24 | } 25 | 26 | func findWinsProcess() (uint32, error) { 27 | h, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) 28 | if err != nil { 29 | panic(err) 30 | } 31 | var p windows.ProcessEntry32 32 | p.Size = uint32(reflect.TypeOf(p).Size()) 33 | 34 | for { 35 | err := windows.Process32Next(h, &p) 36 | if err != nil { 37 | logrus.Errorf("[findWinsProcess] error finding next process: %v", err) 38 | break 39 | } 40 | ps := getProcessName(windows.UTF16ToString(p.ExeFile[:])) 41 | logrus.Debugf("[findWinsProcess] found trimmed process: %v", ps) 42 | if ps == defaults.WindowsProcessName { 43 | pid := p.ProcessID 44 | logrus.Debugf("[findWinsProcess] found process [%s] with pid [%d]", ps, pid) 45 | return pid, nil 46 | } 47 | logrus.Warnf("[findWinsProcess] no process matching [%s] was found", defaults.WindowsProcessName) 48 | } 49 | return 0, nil 50 | } 51 | 52 | func callStackDump() (err error) { 53 | 54 | winsProcessID, err := findWinsProcess() 55 | if err != nil { 56 | return fmt.Errorf("[callStackDump]: error returned when getting wins process id: %v", err) 57 | } 58 | 59 | event := fmt.Sprintf("Global\\stackdump-%d", winsProcessID) 60 | ev, _ := windows.UTF16PtrFromString(event) 61 | 62 | // verify that wins is running before trying to send stackdump signal 63 | if syscall.Signal(syscall.Signal(0)) == 0 { 64 | logrus.Debugf("[callStackDump] confirmed that wins process %d is running", winsProcessID) 65 | } 66 | 67 | sd, err := windows.SecurityDescriptorFromString(defaults.PermissionBuiltinAdministratorsAndLocalSystem) 68 | if err != nil { 69 | return fmt.Errorf("failed to get security descriptor for debug stackdump event %s: %v", event, err) 70 | } 71 | 72 | var sa windows.SecurityAttributes 73 | sa.Length = uint32(unsafe.Sizeof(sa)) 74 | sa.InheritHandle = 1 75 | sa.SecurityDescriptor = sd 76 | 77 | // attempt to open the existing listen event for wins stack dump 78 | h, err := windows.OpenEvent(0x1F0003, // EVENT_ALL_ACCESS 79 | true, 80 | ev) 81 | if err != nil { 82 | return err 83 | } 84 | defer windows.CloseHandle(h) 85 | 86 | if err := windows.SetEvent(h); err != nil { 87 | return fmt.Errorf("[callStackDump] error setting win32 event: %v", err) 88 | } 89 | 90 | return windows.ResetEvent(h) 91 | } 92 | 93 | func getProcessName(path string) string { 94 | return strings.TrimRight(filepath.Base(path), ".exe") 95 | } 96 | -------------------------------------------------------------------------------- /pkg/profilings/profiling.go: -------------------------------------------------------------------------------- 1 | package profilings 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | "runtime/pprof" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func Init(profileName string, profileOutput string) error { 12 | switch profileName { 13 | case "none": 14 | return nil 15 | case "cpu": 16 | f, err := os.Create(profileOutput) 17 | if err != nil { 18 | return err 19 | } 20 | return pprof.StartCPUProfile(f) 21 | case "block": 22 | runtime.SetBlockProfileRate(1) 23 | return nil 24 | case "mutex": 25 | runtime.SetMutexProfileFraction(1) 26 | return nil 27 | default: 28 | if profile := pprof.Lookup(profileName); profile == nil { 29 | return errors.Errorf("unknown profile %q", profileName) 30 | } 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func Flush(profileName string, profileOutput string) error { 37 | switch profileName { 38 | case "none": 39 | return nil 40 | case "cpu": 41 | pprof.StopCPUProfile() 42 | case "heap": 43 | runtime.GC() 44 | fallthrough 45 | default: 46 | profile := pprof.Lookup(profileName) 47 | if profile == nil { 48 | return nil 49 | } 50 | f, err := os.Create(profileOutput) 51 | if err != nil { 52 | return err 53 | } 54 | profile.WriteTo(f, 0) 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/profilings/stack.go: -------------------------------------------------------------------------------- 1 | package profilings 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "runtime" 8 | "unsafe" 9 | 10 | "github.com/rancher/wins/pkg/converters" 11 | "github.com/rancher/wins/pkg/defaults" 12 | "github.com/sirupsen/logrus" 13 | "golang.org/x/sys/windows" 14 | ) 15 | 16 | // DumpStacks returns up to (1 << 15) bytes of the current processes stack trace as a string 17 | func DumpStacks() string { 18 | var ( 19 | buf []byte 20 | stackSize int 21 | bufferLen = 1 << 15 22 | ) 23 | for stackSize == len(buf) { 24 | buf = make([]byte, bufferLen) 25 | stackSize = runtime.Stack(buf, true) 26 | bufferLen *= 2 27 | } 28 | buf = buf[:stackSize] 29 | return converters.UnsafeBytesToString(buf) 30 | } 31 | 32 | // DumpStacksToFile dumps the stack trace of all current goroutines to the file path provided 33 | func DumpStacksToFile(filename string) { 34 | if filename == "" { 35 | return 36 | } 37 | 38 | stacksdump := DumpStacks() 39 | 40 | f, err := os.Create(filename) 41 | if err != nil { 42 | logrus.Errorf("Failed to dump stacks to %s", filename) 43 | return 44 | } 45 | defer f.Close() 46 | f.WriteString(stacksdump) 47 | } 48 | 49 | // SetupDumpStacks creates a goroutine that listens for any signals passed to the Win32 event stackdump-{pid} 50 | // that is defined on a Global level; each time a signal is detected to this event, it will dump the a stack 51 | // trace across all goroutines (up to 1 << 15 bytes) to a file within the Windows machine's temp directory. 52 | // By default, this event can only be signaled by built-in administrators and the local system. 53 | func SetupDumpStacks(serviceName string, pid int, cwd string) { 54 | if serviceName == "" { 55 | return 56 | } 57 | 58 | // Windows does not support signals like *nix systems. So instead of 59 | // trapping on SIGUSR1 to dump stacks, we wait on a Win32 event to be 60 | // signaled. ACL'd to builtin administrators and local system 61 | event := fmt.Sprintf("Global\\stackdump-%d", pid) 62 | ev, _ := windows.UTF16PtrFromString(event) 63 | sd, err := windows.SecurityDescriptorFromString(defaults.PermissionBuiltinAdministratorsAndLocalSystem) 64 | if err != nil { 65 | logrus.Errorf("Failed to get security descriptor for debug stackdump event %s: %v", event, err) 66 | return 67 | } 68 | var sa windows.SecurityAttributes 69 | sa.Length = uint32(unsafe.Sizeof(sa)) 70 | sa.InheritHandle = 1 71 | sa.SecurityDescriptor = sd 72 | h, err := windows.CreateEvent(&sa, 0, 0, ev) 73 | if h == 0 || err != nil { 74 | logrus.Errorf("Failed to create debug stackdump event %s: %v", event, err) 75 | return 76 | } 77 | 78 | go func() { 79 | logrus.Infof("[SetupDumpStacks] stackdump feature successfully initialized - waiting for signal at %s", event) 80 | for { 81 | windows.WaitForSingleObject(h, windows.INFINITE) 82 | fileLoc := filepath.Join(cwd, fmt.Sprintf("%s.%d.stacks.log", serviceName, pid)) 83 | logrus.Debugf("SetupStackDumps: stackDump location will be [%s]", fileLoc) 84 | DumpStacksToFile(fileLoc) 85 | } 86 | }() 87 | } 88 | -------------------------------------------------------------------------------- /pkg/proxy/client.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "github.com/Microsoft/go-winio" 9 | "github.com/gorilla/websocket" 10 | "github.com/rancher/remotedialer" 11 | "github.com/rancher/wins/pkg/npipes" 12 | "inet.af/tcpproxy" 13 | ) 14 | 15 | // NewClientDialer returns a websocket.Dialer that dials a named pipe 16 | func NewClientDialer(path string) (dialer *websocket.Dialer, err error) { 17 | path, err = npipes.ParsePath(path) 18 | if err != nil { 19 | return nil, err 20 | } 21 | return &websocket.Dialer{ 22 | NetDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { 23 | return winio.DialPipeContext(ctx, path) 24 | }, 25 | }, nil 26 | } 27 | 28 | // GetClientConnectAuthorizer returns the client's connect authorizer based on the provided ports 29 | func GetClientConnectAuthorizer(ports []int) remotedialer.ConnectAuthorizer { 30 | validAddresses := make(map[string]bool, len(ports)) 31 | for _, p := range ports { 32 | validAddresses[fmt.Sprintf("localhost:%d", p)] = true 33 | } 34 | return func(proto, address string) bool { 35 | return proto == "tcp" && validAddresses[address] 36 | } 37 | } 38 | 39 | // GetClientOnConnect returns the onConnect function used by the client to set up the tcpproxy 40 | func GetClientOnConnect(ports []int) func(context.Context, *remotedialer.Session) error { 41 | return func(c context.Context, s *remotedialer.Session) error { 42 | proxy := &tcpproxy.Proxy{} 43 | for _, p := range ports { 44 | listenAddress := fmt.Sprintf(":%d", p) 45 | forwardAddress := fmt.Sprintf("localhost:%d", p) 46 | dialContext := func(ctx context.Context, _, _ string) (net.Conn, error) { 47 | return s.Dial(ctx, "tcp", forwardAddress) 48 | } 49 | proxy.AddRoute(listenAddress, &tcpproxy.DialProxy{DialContext: dialContext}) 50 | } 51 | if err := proxy.Start(); err != nil { 52 | return err 53 | } 54 | <-c.Done() 55 | proxy.Close() 56 | return nil 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/proxy/server.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/rancher/remotedialer" 7 | ) 8 | 9 | const ( 10 | // ClientIDHeader is the key used in the HTTP header to identify a given incoming connection to the server 11 | ClientIDHeader = "rancher-wins-cli-proxy" 12 | ) 13 | 14 | // GetServerAuthorizer returns authorizer used to get client information from the request made to the server 15 | func GetServerAuthorizer() remotedialer.Authorizer { 16 | return func(req *http.Request) (clientKey string, authed bool, err error) { 17 | return req.Header.Get(ClientIDHeader), true, nil 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /pkg/syscalls/route.go: -------------------------------------------------------------------------------- 1 | package syscalls 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | ) 7 | 8 | var ( 9 | modiphlpapi = syscall.NewLazyDLL("iphlpapi.dll") 10 | 11 | procGetIPForwardTable = modiphlpapi.NewProc("GetIpForwardTable") 12 | procCreateIPForwardEntry = modiphlpapi.NewProc("CreateIpForwardEntry") 13 | ) 14 | 15 | type IPForwardTable struct { 16 | NumEntries uint32 17 | Table [1]IPForwardRow 18 | } 19 | 20 | type IPForwardRow struct { 21 | ForwardDest uint32 22 | ForwardMask uint32 23 | ForwardPolicy uint32 24 | ForwardNextHop uint32 25 | ForwardIfIndex uint32 26 | ForwardType uint32 27 | ForwardProto uint32 28 | ForwardAge uint32 29 | ForwardNextHopAS uint32 30 | ForwardMetric1 uint32 31 | ForwardMetric2 uint32 32 | ForwardMetric3 uint32 33 | ForwardMetric4 uint32 34 | ForwardMetric5 uint32 35 | } 36 | 37 | func GetIPForwardTable(ft *IPForwardTable, size *uint32, order bool) (errcode error) { 38 | var _p0 uint32 39 | if order { 40 | _p0 = 1 41 | } else { 42 | _p0 = 0 43 | } 44 | r0, _, _ := syscall.Syscall(procGetIPForwardTable.Addr(), 3, uintptr(unsafe.Pointer(ft)), uintptr(unsafe.Pointer(size)), uintptr(_p0)) 45 | if r0 != 0 { 46 | errcode = syscall.Errno(r0) 47 | } 48 | return 49 | } 50 | 51 | func CreateIPForwardEntry(fr *IPForwardRow) (errcode error) { 52 | r0, _, _ := syscall.Syscall(procCreateIPForwardEntry.Addr(), 1, uintptr(unsafe.Pointer(fr)), 0, 0) 53 | if r0 != 0 { 54 | errcode = syscall.Errno(r0) 55 | } 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /pkg/systemagent/agent.go: -------------------------------------------------------------------------------- 1 | package systemagent 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rancher/system-agent/pkg/applyinator" 9 | "github.com/rancher/system-agent/pkg/config" 10 | "github.com/rancher/system-agent/pkg/image" 11 | "github.com/rancher/system-agent/pkg/k8splan" 12 | "github.com/rancher/system-agent/pkg/localplan" 13 | "github.com/rancher/system-agent/pkg/version" 14 | "github.com/sirupsen/logrus" 15 | ) 16 | 17 | type Agent struct { 18 | cfg *config.AgentConfig 19 | StrictTLSMode bool 20 | } 21 | 22 | func (a *Agent) Run(ctx context.Context) error { 23 | 24 | if a.cfg == nil { 25 | logrus.Info("Rancher System Agent configuration not found, not starting system agent.") 26 | return nil 27 | } 28 | 29 | logrus.Infof("Rancher System Agent version %s is starting", version.FriendlyVersion()) 30 | 31 | if !a.cfg.LocalEnabled && !a.cfg.RemoteEnabled { 32 | return errors.New("local and remote were both not enabled. exiting, as one must be enabled") 33 | } 34 | 35 | logrus.Infof("Setting %s as the working directory", a.cfg.WorkDir) 36 | 37 | imageUtil := image.NewUtility(a.cfg.ImagesDir, a.cfg.ImageCredentialProviderConfig, a.cfg.ImageCredentialProviderBinDir, a.cfg.AgentRegistriesFile) 38 | // Currently we do not support the 'interlockDir' on Windows, as the system-agent install script does not yet utilize those files 39 | applier := applyinator.NewApplyinator(a.cfg.WorkDir, a.cfg.PreserveWorkDir, a.cfg.AppliedPlanDir, "", imageUtil) 40 | if a.cfg.RemoteEnabled { 41 | logrus.Infof("Starting remote watch of plans") 42 | logrus.Debugf("Agent Strict TLS Mode is %t", a.StrictTLSMode) 43 | 44 | var connInfo config.ConnectionInfo 45 | 46 | if err := config.Parse(a.cfg.ConnectionInfoFile, &connInfo); err != nil { 47 | return fmt.Errorf("unable to parse connection info file: %v", err) 48 | } 49 | 50 | k8splan.Watch(ctx, *applier, connInfo, a.StrictTLSMode) 51 | } 52 | 53 | if a.cfg.LocalEnabled { 54 | logrus.Infof("Starting local watch of plans in %s", a.cfg.LocalPlanDir) 55 | localplan.WatchFiles(ctx, *applier, a.cfg.LocalPlanDir) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func New(cfg *config.AgentConfig) *Agent { 62 | return &Agent{ 63 | cfg: cfg, 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pkg/tls/tls.go: -------------------------------------------------------------------------------- 1 | package tls 2 | 3 | import ( 4 | "crypto/x509" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type Config struct { 13 | Insecure *bool `yaml:"insecure" json:"insecure,omitempty"` 14 | CertFilePath string `yaml:"certFilePath" json:"certFilePath"` 15 | } 16 | 17 | // SetupGenericTLSConfigFromFile returns a x509 system certificate pool containing the specified certificate file 18 | func (c *Config) SetupGenericTLSConfigFromFile() (*x509.CertPool, error) { 19 | logrus.Infof("Configuring TLS as insecure: %v and using the following cert: %s", c.Insecure, c.CertFilePath) 20 | if c.CertFilePath == "" { 21 | logrus.Info("[SetupGenericTLSConfigFromFile] specified certificate file path is empty, not modifying system certificate store") 22 | return nil, nil 23 | } 24 | 25 | // Get the System certificate store, continue with a new/empty pool on error 26 | systemCerts, err := x509.SystemCertPool() 27 | if err != nil || systemCerts == nil { 28 | logrus.Warnf("[SetupGenericTLSConfigFromFile] failed to return System Cert Pool, creating new one: %v", err) 29 | systemCerts = x509.NewCertPool() 30 | } 31 | 32 | localCertPath, err := os.Stat(c.CertFilePath) 33 | if err != nil { 34 | return nil, fmt.Errorf("[SetupGenericTLSConfigFromFile] unable to read certificate %s from %s: %v", localCertPath.Name(), c.CertFilePath, err) 35 | } 36 | 37 | certs, err := ioutil.ReadFile(c.CertFilePath) 38 | if err != nil { 39 | return nil, fmt.Errorf("[SetupGenericTLSConfigFromFile] failed to read local cert file %q: %v", c.CertFilePath, err) 40 | } 41 | 42 | // Append the specified cert to the system pool 43 | if ok := systemCerts.AppendCertsFromPEM(certs); !ok { 44 | return nil, fmt.Errorf("[SetupGenericTLSConfigFromFile] unable to append cert %s to store, using system certs only", c.CertFilePath) 45 | } 46 | 47 | logrus.Infof("[SetupGenericTLSConfigFromFile] successfully loaded %s certificate into system cert store", c.CertFilePath) 48 | return systemCerts, nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/types/application.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | import "common.proto"; 6 | 7 | option go_package = "types"; 8 | 9 | service ApplicationService { 10 | rpc Info (Void) returns (ApplicationInfoResponse) { 11 | } 12 | } 13 | 14 | message ApplicationInfoResponse { 15 | ApplicationInfo Info = 1; 16 | } 17 | 18 | message ApplicationInfo { 19 | string Checksum = 1; 20 | string Version = 2; 21 | string Commit = 3; 22 | } 23 | -------------------------------------------------------------------------------- /pkg/types/common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | option go_package = "types"; 6 | 7 | message Void { 8 | } 9 | -------------------------------------------------------------------------------- /pkg/types/hns.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | option go_package = "types"; 6 | 7 | service HnsService { 8 | rpc GetNetwork (HnsGetNetworkRequest) returns (HnsGetNetworkResponse) { 9 | } 10 | } 11 | 12 | message HnsGetNetworkRequest { 13 | oneof Options { 14 | string Address = 1; 15 | string Name = 2; 16 | } 17 | } 18 | 19 | message HnsGetNetworkResponse { 20 | HnsNetwork Data = 1; 21 | } 22 | 23 | message HnsNetwork { 24 | string ID = 1; 25 | string Type = 2; 26 | repeated HnsNetworkSubnet Subnets = 3; 27 | string ManagementIP = 4; 28 | } 29 | 30 | message HnsNetworkSubnet { 31 | string AddressCIDR = 1; 32 | string GatewayAddress = 2; 33 | } 34 | -------------------------------------------------------------------------------- /pkg/types/host.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | import "common.proto"; 6 | 7 | option go_package = "types"; 8 | 9 | service HostService { 10 | rpc GetVersion (Void) returns (HostGetVersionResponse) { 11 | } 12 | } 13 | 14 | message HostGetVersionResponse { 15 | HostVersion Data = 1; 16 | } 17 | 18 | message HostVersion { 19 | string CurrentMajorVersionNumber = 1; 20 | string CurrentMinorVersionNumber = 2; 21 | string CurrentBuildNumber = 3; 22 | string UBR = 4; 23 | string ReleaseId = 5; 24 | string BuildLabEx = 6; 25 | string CurrentBuild = 7; 26 | } 27 | -------------------------------------------------------------------------------- /pkg/types/network.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | option go_package = "types"; 6 | 7 | service NetworkService { 8 | rpc Get (NetworkGetRequest) returns (NetworkGetResponse) { 9 | } 10 | } 11 | 12 | message NetworkGetRequest { 13 | oneof Options { 14 | string Address = 1; 15 | string Name = 2; 16 | } 17 | } 18 | 19 | message NetworkGetResponse { 20 | NetworkAdapter Data = 1; 21 | } 22 | 23 | message NetworkAdapter { 24 | string InterfaceIndex = 1; 25 | string GatewayAddress = 2; 26 | string SubnetCIDR = 3; 27 | string HostName = 4; 28 | string AddressCIDR = 5; 29 | } 30 | -------------------------------------------------------------------------------- /pkg/types/process.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | import "common.proto"; 6 | 7 | option go_package = "types"; 8 | 9 | service ProcessService { 10 | rpc Start (ProcessStartRequest) returns (ProcessStartResponse) { 11 | } 12 | rpc Wait (ProcessWaitRequest) returns (stream ProcessWaitResponse) { 13 | } 14 | rpc KeepAlive(stream ProcessKeepAliveRequest) returns (Void) { 15 | } 16 | } 17 | 18 | message ProcessStartRequest { 19 | string Checksum = 1; 20 | string Path = 2; 21 | repeated string Args = 3; 22 | repeated ProcessExpose Exposes = 4; 23 | repeated string Envs = 5; 24 | string Dir = 6; 25 | } 26 | 27 | message ProcessStartResponse { 28 | ProcessName Data = 1; 29 | } 30 | 31 | message ProcessWaitRequest { 32 | ProcessName Data = 1; 33 | } 34 | 35 | message ProcessWaitResponse { 36 | oneof Options { 37 | bytes StdOut = 1; 38 | bytes StdErr = 2; 39 | } 40 | } 41 | 42 | message ProcessKeepAliveRequest { 43 | ProcessName Data = 1; 44 | } 45 | 46 | enum RunExposeProtocol { 47 | TCP = 0; 48 | UDP = 1; 49 | } 50 | 51 | message ProcessExpose { 52 | int32 Port = 1; 53 | RunExposeProtocol Protocol = 2; 54 | } 55 | 56 | message ProcessName { 57 | string Value = 1; 58 | } 59 | -------------------------------------------------------------------------------- /pkg/types/route.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package wins; 4 | 5 | import "common.proto"; 6 | 7 | option go_package = "types"; 8 | 9 | service RouteService { 10 | rpc Add (RouteAddRequest) returns (Void) { 11 | } 12 | } 13 | 14 | message RouteAddRequest { 15 | repeated string Addresses = 1; 16 | } 17 | -------------------------------------------------------------------------------- /scripts/build-image.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Builds the rancher-wins docker image 4 | .DESCRIPTION 5 | Runs the docker build command for a given windows OS version, docker repository, and tag 6 | .NOTES 7 | Parameters: 8 | - NanoServerVersion: Defines the windows OS version used to build the image. Can be either 'ltsc2019' or 'ltsc2022' 9 | - Repo: Dockerhub Repo 10 | - Tag: Docker Tag 11 | 12 | .EXAMPLE 13 | build-image -NanoServerVersion "ltsc2019" -Repo "myRepo" -Tag "v1.2.3" 14 | #> 15 | 16 | param ( 17 | [Parameter()] 18 | [String] 19 | $NanoServerVersion, 20 | 21 | [Parameter()] 22 | [String] 23 | $Repo, 24 | 25 | [Parameter()] 26 | [String] 27 | $Tag 28 | ) 29 | 30 | if (($NanoServerVersion -eq "") -or 31 | (($NanoServerVersion -ne "ltsc2022") -and ($NanoServerVersion -ne "ltsc2019"))) { 32 | Write-Host "-NanoServerVersion must be provided. Accepted values are 'ltsc2019' and 'ltsc2022'" 33 | } 34 | 35 | if ($Repo -eq "") { 36 | Write-Host "Repo paramter is empty, defaulting to 'rancher'" 37 | $Repo = "rancher" 38 | } 39 | 40 | if ($Tag -eq "") { 41 | Write-Host "Tag parameter is empty" 42 | exit 1 43 | } 44 | 45 | # Don't run this command from the scripts directory, always use the parent directory (i.e ./scripts/build-image) 46 | docker build -f Dockerfile --build-arg NANOSERVER_VERSION=$NanoServerVersion --build-arg ARCH=amd64 --build-arg MAINTAINERS="harrison.affel@suse.com" --build-arg REPO=https://github.com/rancher/wins -t $Repo/wins:$Tag-windows-$NanoServerVersion . 47 | -------------------------------------------------------------------------------- /scripts/integration.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 5.0 2 | 3 | $ErrorActionPreference = 'Stop' 4 | 5 | Import-Module -WarningAction Ignore -Name "$PSScriptRoot\utils.psm1" 6 | 7 | $SRC_PATH = (Resolve-Path "$PSScriptRoot\..").Path 8 | Push-Location $SRC_PATH 9 | 10 | Invoke-Expression -Command "$SRC_PATH\tests\integration\integration_suite_test.ps1" 11 | if ($LASTEXITCODE -ne 0) { 12 | Log-Fatal "integration test failed" 13 | exit $LASTEXITCODE 14 | } 15 | Write-Host -ForegroundColor Green "integration.ps1 has completed successfully." 16 | 17 | Pop-Location 18 | -------------------------------------------------------------------------------- /scripts/protoc-gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | # Logging helper functions 5 | info() 6 | { 7 | echo "INFO:" "$@" 1>&2 8 | } 9 | 10 | warn() 11 | { 12 | echo "WARN:" "$@" 1>&2 13 | } 14 | 15 | error() 16 | { 17 | echo "ERROR:" "$@" 1>&2 18 | exit 255 19 | } 20 | 21 | if ! [[ "$0" =~ "scripts/protoc-gen" ]]; then 22 | error "must be run from repository root" 23 | fi 24 | 25 | if ! [[ $(protoc --version) =~ 3.14.0 ]]; then 26 | error "could not find protoc 3.14.0, is it installed + in PATH?" 27 | fi 28 | 29 | GOGOPROTO_ROOT="${GOPATH}/src/github.com/gogo/protobuf" 30 | GOGOPROTO_PATH="${GOGOPROTO_ROOT}:${GOGOPROTO_ROOT}/protobuf" 31 | 32 | DIRS="pkg/types" 33 | 34 | for dir in ${DIRS}; do 35 | pushd ${dir} 1>/dev/null 2>&1 36 | 37 | protoc --gogofaster_out=plugins=grpc:. -I=. \ 38 | -I="${GOGOPROTO_PATH}" \ 39 | *.proto 40 | 41 | if ! [[ $? ]]; then 42 | info "failed to generate $(pwd)/*.proto" 43 | else 44 | info "generated $(pwd)/*.proto" 45 | fi 46 | 47 | popd 1>/dev/null 2>&1 48 | done 49 | 50 | -------------------------------------------------------------------------------- /suc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/mattn/go-colorable" 10 | "github.com/rancher/wins/pkg/defaults" 11 | "github.com/rancher/wins/pkg/panics" 12 | "github.com/rancher/wins/suc/pkg" 13 | "github.com/sirupsen/logrus" 14 | "github.com/urfave/cli/v2" 15 | ) 16 | 17 | func main() { 18 | 19 | defer panics.Log() 20 | app := cli.NewApp() 21 | app.Version = defaults.AppVersion 22 | app.Name = defaults.WindowsSUCName 23 | app.Usage = "A way to modify rancher-wins via the Rancher System Upgrade Controller" 24 | app.Action = pkg.Run 25 | app.Description = fmt.Sprintf(`%s (%s)`, defaults.WindowsSUCName, defaults.AppCommit) 26 | app.Writer = colorable.NewColorableStdout() 27 | app.ErrWriter = colorable.NewColorableStderr() 28 | app.Before = func(c *cli.Context) error { 29 | if c.Bool("debug") || strings.ToLower(os.Getenv("CATTLE_WINS_DEBUG")) == "true" { 30 | logrus.SetLevel(logrus.DebugLevel) 31 | } 32 | if c.Bool("quiet") { 33 | logrus.SetOutput(io.Discard) 34 | } else { 35 | logrus.SetOutput(c.App.Writer) 36 | } 37 | 38 | logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true, FullTimestamp: true}) 39 | return nil 40 | } 41 | 42 | app.Flags = []cli.Flag{ 43 | &cli.BoolFlag{ 44 | Name: "debug", 45 | Usage: "Turn on verbose debug logging", 46 | }, 47 | &cli.BoolFlag{ 48 | Name: "quiet", 49 | Usage: "Turn off all logging", 50 | }, 51 | } 52 | 53 | if err := app.Run(os.Args); err != nil && err != io.EOF { 54 | logrus.Fatal(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /suc/pkg/cmd.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/rancher/wins/suc/pkg/host" 8 | "github.com/rancher/wins/suc/pkg/rancher" 9 | "github.com/rancher/wins/suc/pkg/service" 10 | "github.com/rancher/wins/suc/pkg/service/config" 11 | "github.com/rancher/wins/suc/pkg/state" 12 | "github.com/sirupsen/logrus" 13 | "github.com/urfave/cli/v2" 14 | ) 15 | 16 | func Run(_ *cli.Context) error { 17 | var errs []error 18 | initialState, err := state.BuildInitialState() 19 | if err != nil { 20 | return fmt.Errorf("could not build initial state for rancher-wins: %w", err) 21 | } 22 | 23 | logrus.Info("Updating rancher connection info") 24 | output, err := rancher.UpdateConnectionInformation() 25 | if err != nil { 26 | logrus.Errorf("Could not update rancher connection information") 27 | logrus.Errorf("Script output:\n%s", output) 28 | return fmt.Errorf("error encountered while refreshing connection information: %w", err) 29 | } 30 | 31 | if output != "" { 32 | logrus.Debugf("Script output:\n%s", output) 33 | } 34 | 35 | // update the config using env vars 36 | restartServiceDueToConfigChange, updateErr := config.UpdateConfigFromEnvVars() 37 | if updateErr != nil { 38 | errs = append(errs, updateErr) 39 | } 40 | 41 | // Neither changing the start type nor service 42 | // dependencies require any service restarts 43 | err = service.ConfigureWinsDelayedStart() 44 | if err != nil { 45 | errs = append(errs, err) 46 | } 47 | 48 | err = service.ConfigureRKE2ServiceDependency() 49 | if err != nil { 50 | errs = append(errs, err) 51 | } 52 | 53 | restartServiceDueToBinaryUpgrade, err := host.UpgradeRancherWinsBinary() 54 | if err != nil { 55 | return fmt.Errorf("failed to upgrade wins.exe: %w", err) 56 | } 57 | 58 | if restartServiceDueToConfigChange || restartServiceDueToBinaryUpgrade { 59 | err = service.RefreshWinsService() 60 | if err != nil { 61 | errs = append(errs, fmt.Errorf("error encountered while attempting to restart rancher-wins: %w", err)) 62 | } 63 | } 64 | 65 | if errs != nil && len(errs) > 0 { 66 | logrus.Errorf("Attempting to restore initial state due to error(s) encountered while updating rancher-wins: %v", errors.Join(errs...)) 67 | err = state.RestoreInitialState(initialState) 68 | if err != nil { 69 | errs = append(errs, fmt.Errorf("failed to restore initial state: %w", err)) 70 | } else { 71 | logrus.Info("Successfully restored initial config state") 72 | } 73 | return errors.Join(errs...) 74 | } 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /suc/pkg/host/common_test.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import "testing" 4 | 5 | func TestParseWinsVersion(t *testing.T) { 6 | type test struct { 7 | name string 8 | winsOutput string 9 | expectedVersion string 10 | errExpected bool 11 | } 12 | 13 | tests := []test{ 14 | { 15 | name: "Released version", 16 | winsOutput: "rancher-wins version v0.4.20", 17 | expectedVersion: "v0.4.20", 18 | errExpected: false, 19 | }, 20 | { 21 | name: "RC version", 22 | winsOutput: "rancher-wins version v0.4.20-rc.1", 23 | expectedVersion: "v0.4.20-rc.1", 24 | errExpected: false, 25 | }, 26 | { 27 | name: "Dirty Commit", 28 | winsOutput: "rancher-wins version 06685df-dirty", 29 | expectedVersion: "", 30 | errExpected: true, 31 | }, 32 | { 33 | name: "Unreleased Clean Commit", 34 | winsOutput: "rancher-wins version 06685df", 35 | expectedVersion: "06685df", 36 | errExpected: false, 37 | }, 38 | { 39 | name: "Empty output", 40 | winsOutput: "", 41 | expectedVersion: "", 42 | errExpected: true, 43 | }, 44 | { 45 | name: "unexpected format output", 46 | winsOutput: "rancher-wins version", 47 | expectedVersion: "", 48 | errExpected: true, 49 | }, 50 | } 51 | 52 | for _, tst := range tests { 53 | t.Run(tst.name, func(t *testing.T) { 54 | version, err := parseWinsVersion(tst.winsOutput) 55 | if err != nil && !tst.errExpected { 56 | t.Fatalf("encountered unexpected errror, wins output: '%s', returned version: '%s': %v", tst.winsOutput, version, err) 57 | } 58 | if version != tst.expectedVersion { 59 | t.Fatalf("encountered unexpected version, wins output: '%s', returned version: '%s', expected version: '%s'", tst.winsOutput, version, tst.expectedVersion) 60 | } 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /suc/pkg/host/embed.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import _ "embed" 4 | 5 | //go:embed wins.exe 6 | var winsBinary []byte 7 | -------------------------------------------------------------------------------- /suc/pkg/host/upgrade.go: -------------------------------------------------------------------------------- 1 | package host 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rancher/wins/pkg/defaults" 9 | "github.com/rancher/wins/suc/pkg/service" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // UpgradeRancherWinsBinary will attempt to upgrade the wins.exe binary installed on the host. 14 | // The version to be installed is embedded within the SUC binary, located in the winsBinary variable. 15 | // Upgrades will only be attempted if the CATTLE_WINS_SKIP_BINARY_UPGRADE environment variable is not set to 'true' or '$true', 16 | // and the currently installed version differs from the one embedded (determined by the output of 'wins.exe --version'). 17 | // During an upgrade attempt the rancher-wins service will be temporarily stopped. 18 | // A boolean is returned to indicate if the rancher-wins service needs to be restarted due to a successful upgrade. 19 | func UpgradeRancherWinsBinary() (bool, error) { 20 | if strings.ToLower(os.Getenv(skipBinaryUpgradeEnvVar)) == "true" || 21 | strings.ToLower(os.Getenv(skipBinaryUpgradeEnvVar)) == "$true" { 22 | logrus.Warnf("environment variable '%s' was set to true, will not attempt to upgrade binary", skipBinaryUpgradeEnvVar) 23 | return false, nil 24 | } 25 | 26 | // we use the AppVersion set during compilation to indicate 27 | // the version of wins.exe that is packaged in the SUC binary. 28 | // See magetools/gotool.go for more information. 29 | desiredVersion := defaults.AppVersion 30 | 31 | // We should never install a dirty version of wins.exe onto a host. 32 | if strings.Contains(desiredVersion, "-dirty") { 33 | return false, fmt.Errorf("will not attempt to upgrade wins.exe version, refusing to install embedded dirty version (version: %s)", desiredVersion) 34 | } 35 | 36 | binaryExists, err := confirmWinsBinaryIsInstalled() 37 | if err != nil { 38 | return false, err 39 | } 40 | 41 | if binaryExists { 42 | currentVersion, err := getRancherWinsVersionFromBinary(defaultWinsPath) 43 | if err != nil { 44 | return false, fmt.Errorf("could not determine current wins.exe version: %w", err) 45 | } 46 | 47 | if currentVersion == desiredVersion { 48 | logrus.Debugf("wins.exe is up to date (%s)", currentVersion) 49 | return false, nil 50 | } 51 | } 52 | 53 | restartService, upgradeErr := updateBinaries(desiredVersion) 54 | if upgradeErr != nil { 55 | return false, upgradeErr 56 | } 57 | 58 | return restartService, nil 59 | } 60 | 61 | // updateBinaries writes the embedded binary onto the disk in the rancher-wins config directory (c:\etc\rancher\wins, by default). 62 | // Once written, the binary is invoked to ensure that it is not corrupted and is running the expected version. 63 | // After confirming the version, the updated binary is moved into the wins.exe binary directory ('c:\usr\local\bin', by default) 64 | // and 'c:\Windows' directories. Once the upgraded binary has been moved into place, it is invoked once again 65 | // to confirm the file was copied correctly. 66 | func updateBinaries(desiredVersion string) (bool, error) { 67 | logrus.Info("Writing updated wins.exe to disk") 68 | // write the embedded binary to disk 69 | updatedBinaryPath := fmt.Sprintf("%s/wins-%s.exe", getWinsConfigDir(), strings.Trim(desiredVersion, "\n")) 70 | err := os.WriteFile(updatedBinaryPath, winsBinary, os.ModePerm) 71 | if err != nil { 72 | return false, err 73 | } 74 | 75 | // confirm that the new binary works and returns the version that we expect 76 | err = confirmWinsBinaryVersion(desiredVersion, updatedBinaryPath) 77 | if err != nil { 78 | return false, fmt.Errorf("failed to stage updated binary: %w", err) 79 | } 80 | 81 | logrus.Info("Stopping rancher-wins...") 82 | rw, rwExists, err := service.OpenRancherWinsService() 83 | if err != nil { 84 | return false, fmt.Errorf("failed to open rancher-wins service while attempting to upgrade binary: %w", err) 85 | } 86 | 87 | if rwExists { 88 | // The service needs to be stopped before we can modify the binary it uses 89 | err = rw.Stop() 90 | if err != nil { 91 | return false, fmt.Errorf("failed to stop rancher-wins service while attempting to upgrade binary: %w", err) 92 | } 93 | } 94 | 95 | logrus.Infof("Copying %s to %s", updatedBinaryPath, defaultWinsPath) 96 | err = copyFile(updatedBinaryPath, defaultWinsPath) 97 | if err != nil { 98 | return false, fmt.Errorf("failed to copy new wins.exe binary to %s: %w", defaultWinsPath, err) 99 | } 100 | 101 | // While the rancher-wins service looks for wins.exe in c:\Windows 102 | // for consistency’s sake we should also ensure it's updated in c:\usr\local\bin 103 | // as the install script places it there as well 104 | usrLocalBinPath := getWinsUsrLocalBinBinary() 105 | 106 | logrus.Infof("Copying %s to %s", updatedBinaryPath, usrLocalBinPath) 107 | err = copyFile(updatedBinaryPath, usrLocalBinPath) 108 | if err != nil { 109 | return false, fmt.Errorf("failed to copy new wins.exe binary to %s: %w", usrLocalBinPath, err) 110 | } 111 | 112 | logrus.Infof("Validating updated binaries...") 113 | err = confirmWinsBinaryVersion(desiredVersion, defaultWinsPath) 114 | if err != nil { 115 | return false, err 116 | } 117 | 118 | err = confirmWinsBinaryVersion(desiredVersion, usrLocalBinPath) 119 | if err != nil { 120 | return false, err 121 | } 122 | 123 | logrus.Infof("Removing %s", updatedBinaryPath) 124 | err = os.Remove(updatedBinaryPath) 125 | if err != nil { 126 | return false, fmt.Errorf("failed to remove temporary wins.exe binary (%s): %w", updatedBinaryPath, err) 127 | } 128 | 129 | logrus.Infof("Successfully upgraded wins.exe to version %s", desiredVersion) 130 | return rwExists, nil 131 | } 132 | -------------------------------------------------------------------------------- /suc/pkg/rancher/connectionInfo.go: -------------------------------------------------------------------------------- 1 | package rancher 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "github.com/sirupsen/logrus" 6 | 7 | "fmt" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | const connInfoScriptName = "/hpc/update-connection-info.ps1" 13 | 14 | func UpdateConnectionInformation() (string, error) { 15 | _, err := os.Stat(connInfoScriptName) 16 | if errors.Is(err, os.ErrNotExist) { 17 | logrus.Warnf("Could not find %s, will not attempt to update Rancher connection information", connInfoScriptName) 18 | return "", nil 19 | } else if err != nil { 20 | return "", fmt.Errorf("failed to open %s: %w", connInfoScriptName, err) 21 | } 22 | 23 | // This command is expected to be run in a host process pod. Files packaged into 24 | // containers which are run as host process pods will be accessible from the `/hpc` directory. 25 | cmd := exec.Command("powershell", "-File", connInfoScriptName) 26 | o, err := cmd.CombinedOutput() 27 | if err != nil { 28 | return string(o), fmt.Errorf("failed to update connection info: %w", err) 29 | } 30 | 31 | logrus.Info("Successfully updated connection info") 32 | return string(o), nil 33 | } 34 | -------------------------------------------------------------------------------- /suc/pkg/service/common.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/sirupsen/logrus" 9 | "golang.org/x/sys/windows/svc" 10 | ) 11 | 12 | func getStateTransitionAttempts() int { 13 | env := os.Getenv("CATTLE_WINS_STATE_TRANSITION_ATTEMPTS") 14 | if env != "" { 15 | i, err := strconv.Atoi(env) 16 | if err != nil { 17 | logrus.Debugf("failed to cast 'CATTLE_WINDOWS_STATE_TRANSITION_ATTEMPTS' (%s) to an integer, returning default value of %d", env, stateTransitionAttempts) 18 | return stateTransitionAttempts 19 | } 20 | return i 21 | } 22 | return stateTransitionAttempts 23 | } 24 | 25 | func getStateTransitionDelayInSeconds() time.Duration { 26 | env := os.Getenv("CATTLE_WINS_STATE_TRANSITION_SECONDS") 27 | if env != "" { 28 | i, err := strconv.Atoi(env) 29 | if err != nil { 30 | logrus.Debugf("failed to cast 'CATTLE_WINDOWS_STATE_TRANSITION_SECONDS' (%s) to an integer, returning default value of %d", env, stateTransitionDelayInSeconds) 31 | return stateTransitionDelayInSeconds 32 | } 33 | return time.Duration(i) 34 | } 35 | return stateTransitionDelayInSeconds 36 | } 37 | 38 | func UnorderedSlicesEqual[T comparable](s1 []T, s2 []T) bool { 39 | if len(s1) != len(s2) { 40 | return false 41 | } 42 | for _, e := range s1 { 43 | found := false 44 | for _, e2 := range s2 { 45 | if e == e2 { 46 | found = true 47 | break 48 | } 49 | } 50 | if !found { 51 | return false 52 | } 53 | } 54 | return true 55 | } 56 | 57 | func removeAllFromSlice[T comparable](x T, s []T) []T { 58 | var n []T 59 | for _, e := range s { 60 | if e == x { 61 | continue 62 | } 63 | n = append(n, e) 64 | } 65 | return n 66 | } 67 | 68 | // serviceStateToString translates a svc.State to its string representation 69 | func serviceStateToString(state svc.State) string { 70 | switch state { 71 | case svc.Running: 72 | return "Running" 73 | case svc.Stopped: 74 | return "Stopped" 75 | case svc.StopPending: 76 | return "Stop Pending" 77 | case svc.StartPending: 78 | return "Start Pending" 79 | default: 80 | return "Unknown State" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /suc/pkg/service/common_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import "testing" 4 | 5 | func Test_UnorderedSlicesEqual(t *testing.T) { 6 | type test struct { 7 | name string 8 | sliceA []string 9 | sliceB []string 10 | shouldMatch bool 11 | } 12 | 13 | tests := []test{ 14 | { 15 | name: "Slices match", 16 | sliceA: []string{ 17 | "One", "Two", 18 | }, 19 | sliceB: []string{ 20 | "One", "Two", 21 | }, 22 | shouldMatch: true, 23 | }, 24 | { 25 | name: "Slices match out of order", 26 | sliceA: []string{ 27 | "Two", "One", 28 | }, 29 | sliceB: []string{ 30 | "One", "Two", 31 | }, 32 | shouldMatch: true, 33 | }, 34 | { 35 | name: "Slices do not match", 36 | sliceA: []string{ 37 | "Two", "Three", 38 | }, 39 | sliceB: []string{ 40 | "One", "Two", 41 | }, 42 | shouldMatch: false, 43 | }, 44 | { 45 | name: "Slices do not match due to length difference", 46 | sliceA: []string{ 47 | "Two", 48 | }, 49 | sliceB: []string{ 50 | "One", "Two", 51 | }, 52 | shouldMatch: false, 53 | }, 54 | } 55 | 56 | for _, tst := range tests { 57 | t.Run(tst.name, func(t *testing.T) { 58 | match := UnorderedSlicesEqual(tst.sliceA, tst.sliceB) 59 | if match != tst.shouldMatch { 60 | t.Errorf("Expected %t when determining if slices match, got %t. SliceA %v, SliceB %v", tst.shouldMatch, match, tst.sliceA, tst.sliceB) 61 | } 62 | }) 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /suc/pkg/service/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rancher/wins/cmd/server/config" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | DirEnvVar = "CATTLE_WINS_CONFIG_DIR" 14 | DebugEnvVar = "CATTLE_WINS_DEBUG" 15 | AgentStringTLSEnvVar = "STRICT_VERIFY" 16 | ) 17 | 18 | const ( 19 | defaultConfigFile = "c:/etc/rancher/wins/config" 20 | ) 21 | 22 | func getConfigPath(path string) string { 23 | if path != "" { 24 | return path 25 | } 26 | if path = os.Getenv(DirEnvVar); path != "" { 27 | return path 28 | } 29 | return defaultConfigFile 30 | } 31 | 32 | // LoadConfig utilizes the DirEnvVar environment variable and path parameter to load 33 | // a config file located on the host. If both are provided, the path parameter will take 34 | // precedence. If neither are provided, then the defaultConfigFile path is used. 35 | func LoadConfig(path string) (*config.Config, error) { 36 | cfg := config.DefaultConfig() 37 | err := config.LoadConfig(getConfigPath(path), cfg) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return cfg, nil 43 | } 44 | 45 | // SaveConfig utilizes the DirEnvVar environment variable and path parameter to save 46 | // a config file on the host. If both are provided, the path parameter will take 47 | // precedence. If neither are provided, then the defaultConfigFile path is used. 48 | func SaveConfig(cfg *config.Config, path string) error { 49 | return config.SaveConfig(getConfigPath(path), cfg) 50 | } 51 | 52 | // UpdateConfigFromEnvVars is responsible for updating the rancher-wins config file 53 | // based off of the presence of particular environment variables. The path parameter is used 54 | // to specify the location of the config file on the host. If path is left empty, the defaultConfigFile 55 | // constant is used. The config file will only be updated if a given environment variable is present, and its 56 | // value does not equal the currently set value in the config file. UpdateConfigFromEnvVars returns a boolean 57 | // indicating if the config file has been updated and any errors encountered. 58 | func UpdateConfigFromEnvVars() (bool, error) { 59 | logrus.Info("Loading config from host") 60 | path := getConfigPath("") 61 | cfg, err := LoadConfig(path) 62 | if err != nil { 63 | return false, fmt.Errorf("failed to load config: %v", err) 64 | } 65 | 66 | configNeedsUpdate := false 67 | logrus.Infof("Checking the %s value. This is a boolean flag, expecting 'true' or 'false'", DebugEnvVar) 68 | 69 | v := os.Getenv(DebugEnvVar) 70 | logrus.Infof("Found value '%s' for %s", v, DebugEnvVar) 71 | givenBool := strings.ToLower(v) == "true" 72 | if cfg.Debug != givenBool { 73 | cfg.Debug = givenBool 74 | configNeedsUpdate = true 75 | } 76 | 77 | logrus.Infof("Checking the %s value. This is a boolean flag, expecting 'true' or 'false'", AgentStringTLSEnvVar) 78 | if v := os.Getenv(AgentStringTLSEnvVar); v != "" { 79 | logrus.Infof("Found value '%s' for %s", v, AgentStringTLSEnvVar) 80 | givenBool = strings.ToLower(v) == "true" 81 | if cfg.AgentStrictTLSMode != givenBool { 82 | cfg.AgentStrictTLSMode = givenBool 83 | configNeedsUpdate = true 84 | } 85 | } 86 | 87 | // If we haven't made any changes there is no reason to update the config file 88 | if configNeedsUpdate { 89 | logrus.Info("Detected a change in configuration, updating config file") 90 | err = SaveConfig(cfg, path) 91 | if err != nil { 92 | return configNeedsUpdate, fmt.Errorf("failed to save config: %w", err) 93 | } 94 | } else { 95 | logrus.Info("Did not detect a change in configuration") 96 | } 97 | 98 | return configNeedsUpdate, nil 99 | } 100 | -------------------------------------------------------------------------------- /suc/pkg/service/config/config_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package config 4 | 5 | import ( 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/rancher/wins/cmd/server/config" 15 | v1 "k8s.io/api/core/v1" 16 | ) 17 | 18 | var ( 19 | // configFileLoc denotes where the test 20 | // config will be placed on disk when running in GHA. 21 | // The drive letter is intentionally omitted. 22 | configFileLoc = "" 23 | ) 24 | 25 | func setupTest(vars []v1.EnvVar, t *testing.T) { 26 | h, err := os.UserHomeDir() 27 | if err != nil { 28 | t.Fatal("Could not find user home directory") 29 | } 30 | 31 | configFileLoc = fmt.Sprintf("%s\\wins-test-config", h) 32 | for _, evar := range os.Environ() { 33 | if !strings.Contains(evar, "CATTLE") && evar != "STRICT_VERIFY" { 34 | continue 35 | } 36 | err := os.Unsetenv(strings.Split(evar, "=")[0]) 37 | if err != nil { 38 | t.Fatalf("failed to clear environment variable %s: %v", evar, err) 39 | } 40 | } 41 | 42 | for _, evar := range vars { 43 | err := os.Setenv(evar.Name, evar.Value) 44 | if err != nil { 45 | t.Fatalf("failed to set environment variable %s: %v", evar.Name, err) 46 | } 47 | } 48 | 49 | err = os.Setenv(DirEnvVar, configFileLoc) 50 | if err != nil { 51 | t.Fatalf("Could not set %s", DirEnvVar) 52 | } 53 | 54 | err = os.Remove(configFileLoc) 55 | if err != nil && !errors.Is(err, os.ErrNotExist) { 56 | t.Fatalf("unable to remove existing config file") 57 | } 58 | } 59 | 60 | func Test_UpdateConfigFromEnvVars(t *testing.T) { 61 | type test struct { 62 | name string 63 | envVars []v1.EnvVar 64 | expectedConfig func() *config.Config 65 | updateExpected bool 66 | } 67 | 68 | tests := []test{ 69 | { 70 | name: "Update single known field", 71 | envVars: []v1.EnvVar{ 72 | { 73 | Name: DebugEnvVar, 74 | Value: "true", 75 | }, 76 | }, 77 | expectedConfig: func() *config.Config { 78 | def := config.DefaultConfig() 79 | def.Debug = true 80 | return def 81 | }, 82 | updateExpected: true, 83 | }, 84 | { 85 | name: "No update required", 86 | envVars: []v1.EnvVar{}, 87 | expectedConfig: func() *config.Config { 88 | return config.DefaultConfig() 89 | }, 90 | updateExpected: false, 91 | }, 92 | { 93 | name: "No update due to unknown env var", 94 | envVars: []v1.EnvVar{ 95 | { 96 | Name: "Unknown", 97 | Value: "variable", 98 | }, 99 | }, 100 | expectedConfig: func() *config.Config { 101 | return config.DefaultConfig() 102 | }, 103 | updateExpected: false, 104 | }, 105 | { 106 | name: "Update many known fields", 107 | envVars: []v1.EnvVar{ 108 | { 109 | Name: DebugEnvVar, 110 | Value: "true", 111 | }, 112 | { 113 | Name: AgentStringTLSEnvVar, 114 | Value: "true", 115 | }, 116 | }, 117 | expectedConfig: func() *config.Config { 118 | def := config.DefaultConfig() 119 | def.Debug = true 120 | def.AgentStrictTLSMode = true 121 | return def 122 | }, 123 | updateExpected: true, 124 | }, 125 | } 126 | 127 | for _, tc := range tests { 128 | t.Run(tc.name, func(t *testing.T) { 129 | setupTest(tc.envVars, t) 130 | expectedConfig := tc.expectedConfig() 131 | 132 | updated, err := UpdateConfigFromEnvVars() 133 | if err != nil { 134 | t.Logf("UpdateConfigFromEnvVars returned an unexpected error: %v", err) 135 | t.FailNow() 136 | } 137 | 138 | if updated && !tc.updateExpected { 139 | j, _ := json.MarshalIndent(os.Environ(), "", " ") 140 | t.Logf("Config was updated unexpectedly when the following env vars were used: %s", string(j)) 141 | t.FailNow() 142 | } 143 | 144 | updatedConfig := config.DefaultConfig() 145 | updateConfigErr := config.LoadConfig(configFileLoc, updatedConfig) 146 | if updateConfigErr != nil { 147 | t.Logf("encountered an error when reloading the config file: %v", err) 148 | t.FailNow() 149 | } 150 | 151 | if !reflect.DeepEqual(expectedConfig, updatedConfig) { 152 | j1, _ := json.MarshalIndent(expectedConfig, "", " ") 153 | j2, _ := json.MarshalIndent(updatedConfig, "", " ") 154 | t.Logf("Expected config did not match updated config.\nExpected: %s\nUpdated: %s", string(j1), string(j2)) 155 | t.Fail() 156 | } 157 | }) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /suc/pkg/service/configure.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/rancher/wins/pkg/defaults" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // ConfigureRKE2ServiceDependency creates a service dependency between rke2 and rancher-wins. This results in 13 | // rancher-wins becoming a dependant service for rke2, preventing rke2 startup until rancher-wins is ready. This 14 | // ensures that rancher-wins and rke2 do not interfere one another during start up (For example, due to CNI reconfiguration). 15 | // As a side effect, the rancher-wins service cannot be stopped if rke2 is still running. To restart rancher-wins, 16 | // this dependency must be temporarily removed. 17 | func ConfigureRKE2ServiceDependency() error { 18 | logrus.Info("Configuring rke2 service dependencies") 19 | add := strings.ToLower(os.Getenv("CATTLE_ENABLE_WINS_SERVICE_DEPENDENCY")) == "true" 20 | 21 | rke2, serviceExists, err := OpenRKE2Service() 22 | if err != nil { 23 | return fmt.Errorf("failed to open rke2 service while configuring service dependencies: %w", err) 24 | } 25 | 26 | if !serviceExists { 27 | logrus.Warn("Could not find rke2 service, will not attempt to configure service dependencies") 28 | return nil 29 | } 30 | defer rke2.Close() 31 | 32 | found, err := rke2.HasRancherWinsServiceDependency() 33 | if err != nil { 34 | return fmt.Errorf("error encountered determining rke2 service dependencies: %w", err) 35 | } 36 | 37 | if !found && !add { 38 | logrus.Info("rke2 service dependency not enabled, nothing to do") 39 | return nil 40 | } 41 | 42 | if found && add { 43 | logrus.Info("rke2 service dependency already configured, nothing to do") 44 | return nil 45 | } 46 | 47 | if !found && add { 48 | logrus.Info("Adding rancher-wins dependency on rke2 service") 49 | err = rke2.AddRancherWinsServiceDependency() 50 | if err != nil { 51 | return fmt.Errorf("error encountered adding rke2 service dependency: %w", err) 52 | } 53 | } 54 | 55 | if found && !add { 56 | logrus.Info("Removing rancher-wins dependency on rke2 service") 57 | err = rke2.RemoveRancherWinsServiceDependency() 58 | if err != nil { 59 | return fmt.Errorf("error encountered adding rke2 service dependency: %w", err) 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // ConfigureWinsDelayedStart opens the rancher-wins service and enables the `DelayedAutoStart` flag. 67 | // Enabling this flag does not require a restart of the service. 68 | func ConfigureWinsDelayedStart() error { 69 | logrus.Info("Configuring start type for rancher-wins") 70 | delayedStart := strings.ToLower(os.Getenv("CATTLE_ENABLE_WINS_DELAYED_START")) == "true" 71 | 72 | wins, exists, err := OpenRancherWinsService() 73 | if err != nil { 74 | return fmt.Errorf("failed to open %s service while configuring start type: %w", defaults.WindowsServiceName, err) 75 | } 76 | 77 | if !exists { 78 | logrus.Warnf("could not find the %s service, cannot configure service start type", defaults.WindowsServiceName) 79 | return nil 80 | } 81 | 82 | defer wins.Close() 83 | 84 | err = wins.ConfigureDelayedStart(delayedStart) 85 | if err != nil { 86 | return fmt.Errorf("error encountered configuring delayed start for %s service: %w", defaults.WindowsServiceName, err) 87 | } 88 | 89 | return nil 90 | } 91 | 92 | // RefreshWinsService restarts the rancher-wins service. If a service dependency has 93 | // been configured on the rke2 service, the dependency will be temporarily removed and 94 | // restored once the service restart has completed. 95 | func RefreshWinsService() error { 96 | winSrv, exists, err := OpenRancherWinsService() 97 | if err != nil { 98 | logrus.Errorf("Cannot restart %s as the service failed to open: %v", defaults.WindowsServiceName, err) 99 | return fmt.Errorf("failed to refresh the %s service: %w", winSrv.Name, err) 100 | } 101 | 102 | if !exists { 103 | logrus.Errorf("Cannot restart %s as the service does not exist", defaults.WindowsServiceName) 104 | return nil 105 | } 106 | 107 | defer winSrv.Close() 108 | 109 | // We cannot restart a service which another service depends on. 110 | // In the event that we need to update the rancher-wins config file 111 | // (and thus restart the rancher-wins service), 112 | // we will need to temporarily remove the service dependency from 113 | // the rke2 service if it exists. This ensures that rancher-wins can be updated 114 | // without potentially impacting node functionality due to a restart of rke2. 115 | 116 | rke2Srv, rke2Exists, err := OpenRKE2Service() 117 | if err != nil { 118 | logrus.Errorf("error opening rke2 service while restarting rancher-wins: %v", err) 119 | } 120 | 121 | depRemoved := false 122 | if rke2Exists { 123 | hasDep, err := rke2Srv.HasRancherWinsServiceDependency() 124 | if err != nil { 125 | return fmt.Errorf("error encountered while temporarily removing rke2 service dependency: %w", err) 126 | } 127 | if hasDep { 128 | logrus.Info("Temporarily removing rke2 service dependency") 129 | depRemoved = true 130 | err = rke2Srv.RemoveRancherWinsServiceDependency() 131 | rke2Srv.Close() 132 | if err != nil { 133 | return fmt.Errorf("error encountered while temporarily removing rke2 service dependency: %w", err) 134 | } 135 | } 136 | } 137 | 138 | err = winSrv.Restart() 139 | if err != nil { 140 | return fmt.Errorf("failed to restart the %s service: %w", winSrv.Name, err) 141 | } 142 | 143 | if depRemoved { 144 | // if we removed the dependency then we know the service exists 145 | rke2Srv, _, err = OpenRKE2Service() 146 | if err != nil { 147 | logrus.Errorf("error opening rke2 service while restarting rancher-wins: %v", err) 148 | } 149 | logrus.Info("Restoring rke2 service dependency") 150 | err = rke2Srv.AddRancherWinsServiceDependency() 151 | rke2Srv.Close() 152 | if err != nil { 153 | return fmt.Errorf("error encountered while restoring rke2 service dependency: %w", err) 154 | } 155 | } 156 | 157 | return nil 158 | } 159 | -------------------------------------------------------------------------------- /suc/pkg/service/service_rancher_wins.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/pkg/defaults" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type RancherWinsService struct { 11 | Service 12 | } 13 | 14 | func OpenRancherWinsService() (*RancherWinsService, bool, error) { 15 | winsSvc, exists, err := Open(defaults.WindowsServiceName) 16 | if err != nil { 17 | return nil, false, fmt.Errorf("error encountered opening %s service: %w", defaults.WindowsServiceName, err) 18 | } 19 | 20 | if !exists { 21 | return nil, false, fmt.Errorf("%s service does not exist", defaults.WindowsServiceName) 22 | } 23 | 24 | x := &RancherWinsService{ 25 | *winsSvc, 26 | } 27 | 28 | return x, exists, nil 29 | } 30 | 31 | func (rw *RancherWinsService) ConfigureDelayedStart(enabled bool) error { 32 | logrus.Infof("%s service has delayed auto start configured: %t", defaults.WindowsServiceName, rw.Config.DelayedAutoStart) 33 | if rw.Config.DelayedAutoStart != enabled { 34 | logrus.Infof("updating %s delayed auto start setting to %t", defaults.WindowsServiceName, enabled) 35 | rw.Config.DelayedAutoStart = enabled 36 | err := rw.UpdateConfig() 37 | if err != nil { 38 | return fmt.Errorf("failed to update %s service configuration while configuring service start type: %w", defaults.WindowsServiceName, err) 39 | } 40 | } else { 41 | logrus.Infof("%s delayed start already set to %t, nothing to do", defaults.WindowsServiceName, enabled) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /suc/pkg/service/service_rke2.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rancher/wins/pkg/defaults" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type RKE2Service struct { 11 | Service 12 | } 13 | 14 | func OpenRKE2Service() (*RKE2Service, bool, error) { 15 | rke2Svc, exists, err := Open("rke2") 16 | if err != nil { 17 | return nil, exists, fmt.Errorf("failed to open rke2 service: %w", err) 18 | } 19 | 20 | if !exists { 21 | logrus.Warn("Could not find rke2 service") 22 | return nil, exists, nil 23 | } 24 | 25 | x := &RKE2Service{ 26 | *rke2Svc, 27 | } 28 | 29 | return x, exists, nil 30 | } 31 | 32 | func (rke2 *RKE2Service) HasRancherWinsServiceDependency() (bool, error) { 33 | for _, dep := range rke2.Config.Dependencies { 34 | logrus.Debugf("Found rke2 service dependency '%s'", dep) 35 | if dep == defaults.WindowsServiceName { 36 | logrus.Debug("Found rancher-wins dependency set on rke2 service") 37 | return true, nil 38 | } 39 | } 40 | return false, nil 41 | } 42 | 43 | func (rke2 *RKE2Service) AddRancherWinsServiceDependency() error { 44 | rke2.Config.Dependencies = append(rke2.Config.Dependencies, defaults.WindowsServiceName) 45 | return rke2.UpdateConfig() 46 | } 47 | 48 | func (rke2 *RKE2Service) RemoveRancherWinsServiceDependency() error { 49 | rke2.Config.Dependencies = removeAllFromSlice(defaults.WindowsServiceName, rke2.Config.Dependencies) 50 | if len(rke2.Config.Dependencies) == 0 { 51 | // Updating a service config with a nil or empty Dependencies slice will not have any effect. 52 | // Instead, '/' must be used to clear any remaining service dependencies. 53 | rke2.Config.Dependencies = append(rke2.Config.Dependencies, "/") 54 | } 55 | return rke2.UpdateConfig() 56 | } 57 | -------------------------------------------------------------------------------- /suc/pkg/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | winsConfig "github.com/rancher/wins/cmd/server/config" 8 | "github.com/rancher/wins/pkg/defaults" 9 | "github.com/rancher/wins/suc/pkg/service" 10 | sucConfig "github.com/rancher/wins/suc/pkg/service/config" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | // InitialState represents the configuration of 15 | // rancher-wins and rke2 before any changes are made. 16 | // In the event of an error during reconfiguration of the 17 | // service or the related binaries, this struct should be used 18 | // to roll back all changes. Once an InitialState struct is created 19 | // (via BuildInitialState) it must not be updated. 20 | type InitialState struct { 21 | InitialConfig *winsConfig.Config 22 | InitialServiceConfig Configuration 23 | } 24 | 25 | type Configuration struct { 26 | winsDelayedStart bool 27 | rke2Dependencies []string 28 | } 29 | 30 | // BuildInitialState retrieves the rancher-wins config file and any relevant service 31 | // configuration settings and packages them into an InitialState struct. BuildInitialState 32 | // must be called before any modifications are made to the host to ensure that all changes can be 33 | // safely rolled back. 34 | func BuildInitialState() (InitialState, error) { 35 | logrus.Info("Building Initial State...") 36 | winsCfg, err := sucConfig.LoadConfig("") 37 | if err != nil { 38 | return InitialState{}, fmt.Errorf("could not open rancher-wins config while building initial state: %w", err) 39 | } 40 | 41 | winsSvc, winsExists, err := service.OpenRancherWinsService() 42 | if err != nil { 43 | return InitialState{}, fmt.Errorf("could not open rancher-wins service while building initial state: %w", err) 44 | } 45 | 46 | if !winsExists { 47 | return InitialState{}, fmt.Errorf("the rancher-wins service does not exist") 48 | } 49 | defer winsSvc.Close() 50 | 51 | rke2Svc, rke2Exists, err := service.OpenRKE2Service() 52 | if err != nil { 53 | return InitialState{}, fmt.Errorf("encountered error getting config file for %s service: %w", "rke2", err) 54 | } 55 | 56 | var rke2Deps []string 57 | if rke2Exists { 58 | rke2Deps = rke2Svc.Config.Dependencies 59 | rke2Svc.Close() 60 | } else { 61 | logrus.Warn("Could not find rke2 service while building initial state") 62 | } 63 | 64 | logrus.Debugf("rancher-wins delayed start is set to %t", winsSvc.Config.DelayedAutoStart) 65 | logrus.Debugf("rke2 service dependencies: %v", rke2Deps) 66 | j, err := json.MarshalIndent(winsCfg, "", " ") 67 | if err != nil { 68 | return InitialState{}, fmt.Errorf("could not marshal rancher-wins config to json while building initial state: %w", err) 69 | } 70 | logrus.Debugf("initial rancher-wins config file:\n%s", string(j)) 71 | 72 | return InitialState{ 73 | InitialConfig: winsCfg, 74 | InitialServiceConfig: Configuration{ 75 | winsDelayedStart: winsSvc.Config.DelayedAutoStart, 76 | rke2Dependencies: rke2Deps, 77 | }, 78 | }, nil 79 | } 80 | 81 | // RestoreInitialState will clear all changes made to the host and reinstate the values contained within InitialState. 82 | func RestoreInitialState(state InitialState) error { 83 | var errs []error 84 | // restore rancher-wins service configuration 85 | winsSvc, _, err := service.OpenRancherWinsService() 86 | if err != nil { 87 | errs = append(errs, fmt.Errorf("failed to open %s while restoring initial configuration: %w", defaults.WindowsServiceName, err)) 88 | } 89 | 90 | if winsSvc.Config.DelayedAutoStart != state.InitialServiceConfig.winsDelayedStart { 91 | err = winsSvc.ConfigureDelayedStart(state.InitialServiceConfig.winsDelayedStart) 92 | if err != nil { 93 | errs = append(errs, fmt.Errorf("failed to revert rancher-wins delayed start to %t: %w", state.InitialServiceConfig.winsDelayedStart, err)) 94 | } 95 | } 96 | 97 | // restore rke2 service configuration 98 | saveRke2Config := false 99 | rke2Srv, rke2Exists, err := service.OpenRKE2Service() 100 | if err != nil { 101 | errs = append(errs, fmt.Errorf("failed to open %s while restoring initial configuration: %w", "rke2", err)) 102 | } 103 | 104 | if rke2Exists { 105 | logrus.Infof("Restoring rke2 service configuration") 106 | if !service.UnorderedSlicesEqual(rke2Srv.Config.Dependencies, state.InitialServiceConfig.rke2Dependencies) { 107 | saveRke2Config = true 108 | rke2Srv.Config.Dependencies = state.InitialServiceConfig.rke2Dependencies 109 | } 110 | 111 | if saveRke2Config { 112 | err = rke2Srv.UpdateConfig() 113 | if err != nil { 114 | errs = append(errs, fmt.Errorf("failed to restore initial configuration of %s service: %w", "rke2", err)) 115 | } 116 | } 117 | rke2Srv.Close() 118 | } 119 | 120 | // restore rancher-wins config file 121 | logrus.Infof("Restoring rancher-wins configuration file") 122 | err = sucConfig.SaveConfig(state.InitialConfig, "") 123 | if err != nil { 124 | errs = append(errs, err) 125 | } 126 | 127 | if len(errs) > 0 { 128 | return errors.Join(errs...) 129 | } 130 | 131 | return service.RefreshWinsService() 132 | } 133 | -------------------------------------------------------------------------------- /tests/integration/application_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | $serviceName = "rancher-wins" 3 | 4 | Import-Module -Name @( 5 | "$PSScriptRoot\utils.psm1" 6 | ) -WarningAction Ignore 7 | 8 | # clean interferences 9 | try { 10 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 11 | } 12 | catch { 13 | Log-Warn $_.Exception.Message 14 | } 15 | 16 | Describe "application" { 17 | It "register" { 18 | $ret = .\bin\wins.exe srv app run --register 19 | if (-not $?) { 20 | Log-Error $ret 21 | $false | Should -Be $true 22 | } 23 | 24 | # verify 25 | Get-Service -Name $serviceName -ErrorAction Ignore | Should -Not -BeNullOrEmpty 26 | } 27 | 28 | It "unregister" { 29 | $ret = .\bin\wins.exe srv app run --unregister 30 | if (-not $?) { 31 | Log-Error $ret 32 | $false | Should -Be $true 33 | } 34 | 35 | # verify 36 | Get-Service -Name $serviceName -ErrorAction Ignore | Should -BeNullOrEmpty 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/integration/docker/Dockerfile.wins-cli: -------------------------------------------------------------------------------- 1 | ARG SERVERCORE_VERSION 2 | 3 | FROM mcr.microsoft.com/windows/servercore:${SERVERCORE_VERSION} 4 | COPY bin/wins.exe /Windows/wins.exe 5 | ENTRYPOINT wins.exe cli 6 | -------------------------------------------------------------------------------- /tests/integration/docker/Dockerfile.wins-nginx: -------------------------------------------------------------------------------- 1 | ARG SERVERCORE_VERSION 2 | 3 | FROM mcr.microsoft.com/windows/servercore:${SERVERCORE_VERSION} 4 | SHELL ["powershell", "-NoLogo", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue';"] 5 | 6 | COPY tests/integration/bin/nginx.zip /nginx.zip 7 | RUN Write-Host 'Expanding ...'; \ 8 | Expand-Archive -Force -Path c:\nginx.zip -DestinationPath c:\etc\.; \ 9 | Rename-Item -Force -Path c:\etc\nginx-1.21.3 -NewName nginx; \ 10 | \ 11 | Write-Host 'Cleaning ...'; \ 12 | Remove-Item -Force -Path c:\nginx.zip | Out-Null; \ 13 | \ 14 | Write-Host 'Complete.' 15 | COPY bin/wins.exe /Windows/wins.exe 16 | COPY tests/integration/docker/nginx.ps1 /Windows/ 17 | ENTRYPOINT ["powershell", "-NoLogo", "-NonInteractive", "-File", "c:/Windows/nginx.ps1"] 18 | -------------------------------------------------------------------------------- /tests/integration/docker/nginx.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # copy to host 4 | Copy-Item -Recurse -Force -Path c:\etc\nginx\* -Destination c:\host\etc\nginx\ | Out-Null 5 | 6 | # start process 7 | wins.exe cli prc run --path c:\etc\nginx\nginx.exe --exposes TCP:80 --args=`-g env hello=world` 8 | -------------------------------------------------------------------------------- /tests/integration/hns_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\hns.psm1" 5 | "$PSScriptRoot\utils.psm1" 6 | ) -WarningAction Ignore 7 | 8 | # clean interferences 9 | try { 10 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 11 | } 12 | catch { 13 | Log-Warn $_.Exception.Message 14 | } 15 | 16 | Describe "hns" { 17 | 18 | BeforeEach { 19 | # start wins server 20 | Execute-Binary -FilePath "bin\wins.exe" -ArgumentList @('srv', 'app', 'run') -Backgroud | Out-Null 21 | Wait-Ready -Path //./pipe/rancher_wins 22 | 23 | # create a HNS network 24 | try { 25 | New-HNSNetwork -Type "L2Bridge" -AddressPrefix "192.168.255.0/30" -Gateway "192.168.255.1" -Name "test-cbr0" | Out-Null 26 | } 27 | catch {} 28 | while ($true) { 29 | $n = Get-HnsNetwork -ErrorAction Ignore | Where-Object { $_.Name -eq "test-cbr0" } 30 | if ($n) { 31 | Start-Sleep -Seconds 5 32 | break 33 | } 34 | Start-Sleep -Seconds 1 35 | } 36 | } 37 | 38 | AfterEach { 39 | # clean HNS network 40 | Get-HnsNetwork -ErrorAction Ignore | Where-Object { $_.Name -eq "test-cbr0" } | Remove-HnsNetwork -ErrorAction Ignore 41 | while ($true) { 42 | $n = Get-HnsNetwork -ErrorAction Ignore | Where-Object { $_.Name -eq "test-cbr0" } 43 | if (-not $n) { 44 | Start-Sleep -Seconds 5 45 | break 46 | } 47 | Start-Sleep -Seconds 1 48 | } 49 | 50 | # clean wins server 51 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 52 | } 53 | 54 | It "get-network by name" { 55 | # wins.exe cli hns get-network --name "xxx" 56 | # docker run --name get-network --rm -v //./pipe/rancher_wins://./pipe/rancher_wins -v c:/etc/rancher/wins:c:/etc/rancher/wins wins-cli hns get-network --name test-cbr0 57 | $ret = Execute-Binary -FilePath "docker.exe" -ArgumentList @("run", "--rm", "-v", "//./pipe/rancher_wins://./pipe/rancher_wins", "-v", "c:/etc/rancher/wins:c:/etc/rancher/wins", "wins-cli", "hns", "get-network", "--name", "test-cbr0") -PassThru 58 | if (-not $ret.Ok) { 59 | Log-Error $ret.Output 60 | $false | Should -Be $true 61 | } 62 | 63 | # verify 64 | $expectedObj = $ret.Output | ConvertFrom-Json 65 | $actualObj = Get-HnsNetwork | Where-Object Name -eq "test-cbr0" 66 | $expectedObj.Subnets[0].AddressCIDR -eq $actualObj.Subnets[0].AddressPrefix | Should -Be $true 67 | $expectedObj.Subnets[0].GatewayAddress -eq $actualObj.Subnets[0].GatewayAddress | Should -Be $true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/integration/host_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\utils.psm1" 5 | ) -WarningAction Ignore 6 | 7 | # clean interferences 8 | try { 9 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 10 | } 11 | catch { 12 | Log-Warn $_.Exception.Message 13 | } 14 | 15 | Describe "host" { 16 | 17 | BeforeEach { 18 | # start wins server 19 | Execute-Binary -FilePath "bin\wins.exe" -ArgumentList @('srv', 'app', 'run') -Backgroud | Out-Null 20 | Wait-Ready -Path //./pipe/rancher_wins 21 | } 22 | 23 | AfterEach { 24 | # clean wins server 25 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 26 | } 27 | 28 | It "get version" { 29 | # wins.exe cli host get-version 30 | # docker run --rm -v //./pipe/rancher_wins://./pipe/rancher_wins -v c:/etc/rancher/wins:c:/etc/rancher/wins wins-cli host get-version 31 | $ret = Execute-Binary -FilePath "docker.exe" -ArgumentList @("run", "--rm", "-v", "//./pipe/rancher_wins://./pipe/rancher_wins", "-v", "c:/etc/rancher/wins:c:/etc/rancher/wins", "wins-cli", "host", "get-version") -PassThru 32 | if (-not $ret.Ok) { 33 | Log-Error $ret.Output 34 | $false | Should -Be $true 35 | } 36 | 37 | # verify 38 | $expectedObj = $ret.Output | ConvertFrom-Json 39 | $actualObj = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\' | Select-Object -Property CurrentMajorVersionNumber, CurrentMinorVersionNumber, CurrentBuildNumber, UBR, ReleaseId, BuildLabEx, CurrentBuild 40 | $actualObj.CurrentMajorVersionNumber -eq $expectedObj.CurrentMajorVersionNumber | Should -Be $true 41 | $actualObj.CurrentMinorVersionNumber -eq $expectedObj.CurrentMinorVersionNumber | Should -Be $true 42 | $actualObj.CurrentBuildNumber -eq $expectedObj.CurrentBuildNumber | Should -Be $true 43 | $actualObj.UBR -eq $expectedObj.UBR | Should -Be $true 44 | $actualObj.ReleaseId -eq $expectedObj.ReleaseId | Should -Be $true 45 | $actualObj.BuildLabEx -eq $expectedObj.BuildLabEx | Should -Be $true 46 | $actualObj.CurrentBuild -eq $expectedObj.CurrentBuild | Should -Be $true 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tests/integration/install_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\utils.psm1" 5 | ) -WarningAction Ignore 6 | 7 | # clean interferences 8 | try { 9 | Get-Process -Name "rancher-wins-*" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 10 | Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*" -ErrorAction Ignore | ForEach-Object { Remove-NetFirewallRule -Name $_.Name -PolicyStore ActiveStore -ErrorAction Ignore } | Out-Null 11 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 12 | } 13 | catch { 14 | Log-Warn $_.Exception.Message 15 | } 16 | 17 | Describe "install" { 18 | BeforeEach { 19 | # note: we cannot test system agent install today since we need a mocked API server 20 | Log-Info "Running install script" 21 | # note: Simply running the install script does not do anything. During normal provisioning, 22 | # Rancher will mutate the install script to both add environment variables, and to call 23 | # the primary function 'Invoke-WinsInstaller'. As this is an integration test, we need to manually 24 | # update the install script ourselves. 25 | Add-Content -Path ./install.ps1 -Value '$env:CATTLE_REMOTE_ENABLED = "false"' 26 | Add-Content -Path ./install.ps1 -Value '$env:CATTLE_LOCAL_ENABLED = "true"' 27 | Add-Content -Path ./install.ps1 -Value Invoke-WinsInstaller 28 | 29 | .\install.ps1 30 | } 31 | 32 | AfterEach { 33 | Log-Info "Running uninstall script" 34 | try { 35 | # note: since this script may not be run by an administrator, it's possible that it might fail 36 | # on trying to delete certain files with ACLs attached to them. 37 | # If you are running this locally, make sure you run with admin privileges. 38 | .\uninstall.ps1 39 | } catch { 40 | Log-Warn "You need to manually run uninstall.ps1, encountered error: $($_.Exception.Message)" 41 | } 42 | } 43 | 44 | It "creates files and directories with scoped down permissions" { 45 | # While these get set in install.ps1, pester removes them as 46 | # install.ps1 is called in the BeforeEach block 47 | $env:CATTLE_AGENT_VAR_DIR = "c:/var/lib/rancher/agent" 48 | $env:CATTLE_AGENT_CONFIG_DIR = "c:/etc/rancher/wins" 49 | 50 | $restrictedPaths = @( 51 | $env:CATTLE_AGENT_VAR_DIR, 52 | $env:CATTLE_AGENT_CONFIG_DIR, 53 | "$env:CATTLE_AGENT_CONFIG_DIR/config" 54 | 55 | # TODO: to test the creation of rancher2_connection_info.json, we need to mock the Rancher server. 56 | # Once this capability is added to tests, uncomment this and remove $env:CATTLE_REMOTE_ENABLED = "false" above. 57 | # "$env:CATTLE_AGENT_VAR_DIR/rancher2_connection_info.json" 58 | ) 59 | foreach ($path in $restrictedPaths) { 60 | Log-Info "Checking $path" 61 | 62 | Test-Path -Path $path | Should -Be $true 63 | 64 | Test-Permissions -Path $path -ExpectedOwner "BUILTIN\Administrators" -ExpectedGroup "NT AUTHORITY\SYSTEM" -ExpectedPermissions @( 65 | [PSCustomObject]@{ 66 | AccessMask = "FullControl" 67 | Type = 0 68 | Identity = "NT AUTHORITY\SYSTEM" 69 | }, 70 | [PSCustomObject]@{ 71 | AccessMask = "FullControl" 72 | Type = 0 73 | Identity = "BUILTIN\Administrators" 74 | } 75 | ) 76 | 77 | Log-Info "Confirmed expected ACLs on $path" 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /tests/integration/integration_suite_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | # docker build 4 | $buildTags = @{ "17763" = "1809"; "20348" = "ltsc2022";} 5 | $buildNumber = (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\' -ErrorAction Ignore).CurrentBuildNumber 6 | $SERVERCORE_VERSION = $buildTags[$buildNumber] 7 | if (-not $SERVERCORE_VERSION) { 8 | $SERVERCORE_VERSION = "1809" 9 | } 10 | 11 | Write-Host "Server core version: $SERVERCORE_VERSION" 12 | 13 | $NGINX_URL = 'https://nginx.org/download/nginx-1.21.3.zip'; 14 | Write-Host ('Downloading Nginx from {0}...' -f $NGINX_URL); 15 | 16 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; 17 | Invoke-WebRequest -UseBasicParsing -OutFile $PSScriptRoot\bin\nginx.zip -Uri $NGINX_URL; 18 | 19 | Get-ChildItem -Path $PSScriptRoot\docker -Name Dockerfile.* | ForEach-Object { 20 | $dockerfile = $_ 21 | $tag = $dockerfile -replace "Dockerfile.", "" 22 | Write-Host "Building $tag from $_" 23 | 24 | docker build ` 25 | --build-arg SERVERCORE_VERSION=$SERVERCORE_VERSION ` 26 | -t $tag ` 27 | -f $PSScriptRoot\docker\$dockerfile . 28 | if ($LASTEXITCODE -ne 0) { 29 | Log-Fatal "Failed to build testing docker image" 30 | exit $LASTEXITCODE 31 | } 32 | } 33 | 34 | # test 35 | New-Item -Type Directory -Force -ErrorAction Ignore -Path @( 36 | "c:\etc\rancher\wins" 37 | ) | Out-Null 38 | Get-ChildItem -Path $PSScriptRoot -Name *.ps1 -Exclude $MyInvocation.MyCommand.Name | ForEach-Object { 39 | Invoke-Expression -Command "$PSScriptRoot\$_" 40 | if ($LASTEXITCODE -ne 0) { 41 | Log-Fatal "Failed to pass $PSScriptRoot\$_" 42 | exit $LASTEXITCODE 43 | } 44 | } 45 | Remove-Item -Recurse -Force -ErrorAction Ignore -Path @( 46 | "c:\etc\rancher\wins" 47 | ) 48 | -------------------------------------------------------------------------------- /tests/integration/network_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\utils.psm1" 5 | ) -WarningAction Ignore 6 | 7 | # clean interferences 8 | try { 9 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 10 | } 11 | catch { 12 | Log-Warn $_.Exception.Message 13 | } 14 | 15 | function ConvertTo-MaskLength { 16 | param( 17 | [Parameter(Mandatory = $true, Position = 0)] 18 | [Net.IPAddress] $subnetMask 19 | ) 20 | 21 | $bits = "$($subnetMask.GetAddressBytes() | % { 22 | [Convert]::ToString($_, 2) 23 | } )" -replace "[\s0]" 24 | 25 | return $bits.Length 26 | } 27 | 28 | function ConvertTo-DecimalIP { 29 | param( 30 | [Parameter(Mandatory = $true, Position = 0)] 31 | [Net.IPAddress] $ipAddress 32 | ) 33 | 34 | $i = 3 35 | $decimalIP = 0 36 | 37 | $ipAddress.GetAddressBytes() | % { 38 | $decimalIP += $_ * [Math]::Pow(256, $i) 39 | $i-- 40 | } 41 | 42 | return [UInt32]$decimalIP 43 | } 44 | 45 | function ConvertTo-DottedIP { 46 | param( 47 | [Parameter(Mandatory = $true, Position = 0)] 48 | [Uint32] $ipAddress 49 | ) 50 | 51 | $dottedIP = $(for ($i = 3; $i -gt -1; $i--) { 52 | $base = [Math]::Pow(256, $i) 53 | $remainder = $ipAddress % $base 54 | ($ipAddress - $remainder) / $base 55 | $ipAddress = $remainder 56 | }) 57 | 58 | return [String]::Join(".", $dottedIP) 59 | } 60 | 61 | Describe "network" { 62 | 63 | BeforeEach { 64 | # start wins server 65 | Execute-Binary -FilePath "bin\wins.exe" -ArgumentList @('srv', 'app', 'run') -Backgroud | Out-Null 66 | Wait-Ready -Path //./pipe/rancher_wins 67 | } 68 | 69 | AfterEach { 70 | # clean wins server 71 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 72 | } 73 | 74 | It "get default adapter" { 75 | # wins.exe cli network get 76 | # docker run --rm -v //./pipe/rancher_wins://./pipe/rancher_wins -v c:/etc/rancher/wins:c:/etc/rancher/wins wins-cli network get 77 | New-Directory "c:/etc/rancher/pipe" 78 | 79 | $ret = Execute-Binary -FilePath "docker.exe" -ArgumentList @("run", "--rm", "-v", "//./pipe/rancher_wins://./pipe/rancher_wins", "-v", "c:/etc/rancher/pipe:c:/etc/rancher/pipe", "wins-cli", "network", "get") -PassThru 80 | if (-not $ret.Ok) { 81 | Log-Error $ret.Output 82 | $false | Should -Be $true 83 | } 84 | 85 | # verify 86 | $defaultNetIndex = (Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Ignore | Get-NetAdapter -ErrorAction Ignore | Get-NetRoute -DestinationPrefix "0.0.0.0/0" -ErrorAction Ignore | Select-Object -ExpandProperty ifIndex -First 1) 87 | $expectedObj = $ret.Output | ConvertFrom-Json 88 | $actaulObj = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True and InterfaceIndex=$defaultNetIndex" | Select-Object -Property DefaultIPGateway, DNSHostName, InterfaceIndex, IPAddress, IPSubnet) 89 | $actaulObjSubnetMask = ConvertTo-MaskLength $actaulObj.IPSubnet[0] 90 | $actaulObjSubnetAddr = ConvertTo-DottedIP ((ConvertTo-DecimalIP $actaulObj.IPAddress[0]) -band (ConvertTo-DecimalIP $actaulObj.IPSubnet[0])) 91 | $expectedObj.GatewayAddress -eq $actaulObj.DefaultIPGateway[0] | Should -Be $true 92 | $expectedObj.InterfaceIndex -eq $actaulObj.InterfaceIndex | Should -Be $true 93 | $expectedObj.SubnetCIDR -eq "$actaulObjSubnetAddr/$actaulObjSubnetMask" | Should -Be $true 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /tests/integration/process_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\utils.psm1" 5 | ) -WarningAction Ignore 6 | 7 | # clean interferences 8 | try { 9 | Get-Process -Name "rancher-wins-*" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 10 | Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*" -ErrorAction Ignore | ForEach-Object { Remove-NetFirewallRule -Name $_.Name -PolicyStore ActiveStore -ErrorAction Ignore } | Out-Null 11 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 12 | } 13 | catch { 14 | Log-Warn $_.Exception.Message 15 | } 16 | 17 | Describe "process" { 18 | 19 | BeforeEach { 20 | # create nginx dir 21 | New-Item -ItemType Directory -Path "c:\etc\nginx" -Force -ErrorAction Ignore | Out-Null 22 | } 23 | 24 | AfterEach { 25 | # clean nginx dir 26 | Remove-Item -Path "c:\etc\nginx" -Recurse -Force -ErrorAction Ignore 27 | 28 | # stop nginx process & firewall rules 29 | Get-Process -Name "rancher-wins-*" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 30 | Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*" -ErrorAction Ignore | ForEach-Object { Remove-NetFirewallRule -Name $_.Name -PolicyStore ActiveStore -ErrorAction Ignore } | Out-Null 31 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 32 | } 33 | 34 | # It "run" { 35 | # # generated config 36 | # $config = @{ 37 | # whiteList = @{ 38 | # processPaths = @( 39 | # "C:\otherpath" 40 | # "C:\etc\nginx\nginx.exe" 41 | # ) 42 | # } 43 | # } 44 | # $config | ConvertTo-Json -Compress -Depth 32 | Out-File -NoNewline -Encoding utf8 -Force -FilePath "c:\etc\rancher\wins\config" 45 | # $configJson = Get-Content -Raw -Path "c:\etc\rancher\wins\config" 46 | # Log-Info $configJson 47 | 48 | # # start wins server 49 | # Execute-Binary -FilePath "bin\wins.exe" -ArgumentList @('srv', 'app', 'run') -Backgroud | Out-Null 50 | # Wait-Ready -Path //./pipe/rancher_wins 51 | 52 | # # wins.exe cli prc run --path xxx --exposes xxx 53 | # # docker run --name prc-run --rm -v //./pipe/rancher_wins://./pipe/rancher_wins -v c:/etc/rancher/wins:c:/etc/rancher/wins -v c:/etc/nginx:c:/host/etc/nginx wins-nginx 54 | # Execute-Binary -FilePath "docker.exe" -ArgumentList @("run", "--name", "prc-run", "--rm", "-v", "//./pipe/rancher_wins://./pipe/rancher_wins", "-v", "c:/etc/rancher/wins:c:/etc/rancher/wins", "-v", "c:/etc/nginx:c:/host/etc/nginx", "wins-nginx") -Backgroud 55 | # { 56 | # Wait-Ready -Path "c:\etc\nginx\rancher-wins-nginx.exe" -Throw 57 | # } | Should -Not -Throw 58 | 59 | # # verify running 60 | # { 61 | # # should be abled to find processes 62 | # { 63 | # Get-Process -Name "rancher-wins-*" -ErrorAction Ignore 64 | # } | Judge -Throw -Timeout 120 65 | # } | Should -Not -Throw 66 | # $statusCode = $(curl.exe -sL -w "%{http_code}" -o /dev/null http://127.0.0.1) 67 | # $statusCode | Should -Be 200 68 | # Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*-TCP-80" -ErrorAction Ignore | Should -Not -BeNullOrEmpty 69 | 70 | # # verify stopping 71 | # Execute-Binary -FilePath "docker.exe" -ArgumentList @("rm", "-f", "prc-run") -PassThru | Out-Null 72 | # { 73 | # # should not be abled to find processes 74 | # { 75 | # Get-Process -Name "rancher-wins-*" -ErrorAction Ignore 76 | # } | Judge -Reverse -Throw 77 | # } | Should -Not -Throw 78 | # Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*-TCP-80" -ErrorAction Ignore | Should -BeNullOrEmpty 79 | # } 80 | 81 | It "run not in whitelist" { 82 | # generated config 83 | $config = @{ 84 | white_List = @{ 85 | processPaths = @( 86 | "C:\otherpath" 87 | ) 88 | } 89 | } 90 | New-Directory "c:\etc\rancher\wins" 91 | $config | ConvertTo-Json -Compress -Depth 32 | Out-File -NoNewline -Encoding utf8 -Force -FilePath "c:\etc\rancher\wins\config" 92 | $configJson = Get-Content -Raw -Path "c:\etc\rancher\wins\config" 93 | Log-Info $configJson 94 | 95 | # start wins server 96 | Execute-Binary -FilePath "bin\wins.exe" -ArgumentList @('srv', 'app', 'run') -Backgroud | Out-Null 97 | Wait-Ready -Path //./pipe/rancher_wins 98 | 99 | # wins.exe cli prc run --path xxx --exposes xxx 100 | # docker run --name prc-run --rm -v //./pipe/rancher_wins://./pipe/rancher_wins -v c:/etc/rancher/wins:c:/etc/rancher/wins -v c:/etc/nginx:c:/host/etc/nginx wins-nginx 101 | Execute-Binary -FilePath "docker.exe" -ArgumentList @("run", "--name", "prc-run", "--rm", "-v", "//./pipe/rancher_wins://./pipe/rancher_wins", "-v", "c:/etc/rancher/pipe:c:/etc/rancher/pipe", "-v", "c:/etc/nginx:c:/host/etc/nginx", "wins-nginx") -Backgroud 102 | { 103 | Wait-Ready -Timeout 3 -Path "c:\etc\nginx\rancher-wins-nginx.exe" -Throw 104 | } | Should -Throw 105 | 106 | # verify 107 | { 108 | # should be abled to find processes 109 | { 110 | Get-Process -Name "rancher-wins-*" -ErrorAction Ignore 111 | } | Judge -Timeout 3 -Throw 112 | } | Should -Throw 113 | Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*-TCP-80" -ErrorAction Ignore | Should -BeNullOrEmpty 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/integration/route_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\utils.psm1" 5 | ) -WarningAction Ignore 6 | 7 | # clean interferences 8 | try { 9 | Get-NetRoute -PolicyStore ActiveStore | Where-Object { ($_.DestinationPrefix -eq "7.7.7.7/32") } | ForEach-Object { Remove-NetRoute -Confirm:$false -InterfaceIndex $_.ifIndex -DestinationPrefix $_.DestinationPrefix -NextHop $_.NextHop -PolicyStore ActiveStore -ErrorAction Stop | Out-Null } 10 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 11 | } 12 | catch { 13 | Log-Warn $_.Exception.Message 14 | } 15 | 16 | Describe "route" { 17 | 18 | BeforeEach { 19 | # start wins server 20 | Execute-Binary -FilePath "bin\wins.exe" -ArgumentList @('srv', 'app', 'run') -Backgroud | Out-Null 21 | Wait-Ready -Path //./pipe/rancher_wins 22 | } 23 | 24 | AfterEach { 25 | # clean wins server 26 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 27 | 28 | # clean route 29 | Get-NetRoute -PolicyStore ActiveStore | Where-Object { ($_.DestinationPrefix -eq "7.7.7.7/32") } | ForEach-Object { Remove-NetRoute -Confirm:$false -InterfaceIndex $_.ifIndex -DestinationPrefix $_.DestinationPrefix -NextHop $_.NextHop -PolicyStore ActiveStore -ErrorAction Stop | Out-Null } 30 | } 31 | 32 | It "add" { 33 | # wins.exe cli route add --addresses "7.7.7.7" 34 | # docker run --rm -v //./pipe/rancher_wins://./pipe/rancher_wins -v c:/etc/rancher/wins:c:/etc/rancher/wins wins-cli route add 35 | $ret = Execute-Binary -FilePath "docker.exe" -ArgumentList @("run", "--rm", "-v", "//./pipe/rancher_wins://./pipe/rancher_wins", "-v", "c:/etc/rancher/wins:c:/etc/rancher/wins", "wins-cli", "route", "add", "--addresses", "7.7.7.7") -PassThru 36 | if (-not $ret.Ok) { 37 | Log-Error $ret.Output 38 | $false | Should -Be $true 39 | } 40 | 41 | # verify 42 | Start-Sleep -Seconds 5 43 | Get-NetRoute -PolicyStore ActiveStore | Where-Object { ($_.DestinationPrefix -eq "7.7.7.7/32") } | Measure-Object | Select-Object -ExpandProperty Count | Should -Be 1 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /tests/integration/suc_upgrade_test.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = "Stop" 2 | 3 | Import-Module -Name @( 4 | "$PSScriptRoot\utils.psm1" 5 | ) -WarningAction Ignore 6 | 7 | # clean interferences 8 | try { 9 | Get-Process -Name "rancher-wins-*" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 10 | Get-NetFirewallRule -PolicyStore ActiveStore -Name "rancher-wins-*" -ErrorAction Ignore | ForEach-Object { Remove-NetFirewallRule -Name $_.Name -PolicyStore ActiveStore -ErrorAction Ignore } | Out-Null 11 | Get-Process -Name "wins" -ErrorAction Ignore | Stop-Process -Force -ErrorAction Ignore 12 | } 13 | catch { 14 | Log-Warn $_.Exception.Message 15 | } 16 | 17 | Describe "install" { 18 | BeforeEach { 19 | Log-Info "Running install script" 20 | # note: Simply running the install script does not do anything. During normal provisioning, 21 | # Rancher will mutate the install script to both add environment variables, and to call 22 | # the primary function 'Invoke-WinsInstaller'. As this is an integration test, we need to manually 23 | # update the install script ourselves. 24 | Add-Content -Path ./install.ps1 -Value '$env:CATTLE_REMOTE_ENABLED = "false"' 25 | Add-Content -Path ./install.ps1 -Value '$env:CATTLE_LOCAL_ENABLED = "true"' 26 | Add-Content -Path ./install.ps1 -Value Invoke-WinsInstaller 27 | 28 | .\install.ps1 29 | } 30 | 31 | AfterEach { 32 | Log-Info "Running uninstall script" 33 | try 34 | { 35 | # note: since this script may not be run by an administrator, it's possible that it might fail 36 | # on trying to delete certain files with ACLs attached to them. 37 | # If you are running this locally, make sure you run with admin privileges. 38 | .\uninstall.ps1 39 | } 40 | catch 41 | { 42 | Log-Warn "You need to manually run uninstall.ps1, encountered error: $( $_.Exception.Message )" 43 | } 44 | } 45 | 46 | It "Installs and upgrades" { 47 | $env:CATTLE_WINS_STATE_TRANSITION_ATTEMPTS = 60 48 | $env:CATTLE_WINS_STATE_TRANSITION_SECONDS = 1 49 | 50 | # We currently have the latest release installed, we now need to test upgrading to our version. 51 | # Get the expected version of the new wins.exe binary. On PR's this 52 | # will be a commit hash, and on tag runs it should be a full version (v0.x.y[-rc.z]). 53 | $CIVersion = Get-LatestCommitOrTag 54 | Log-Info "Incoming wins.exe CI version: $CIVersion" 55 | 56 | # Get the currently installed version string 57 | $fullVersion = $(c:\Windows\wins.exe --version) 58 | Log-Info "Current wins.exe version installed: $fullVersion" 59 | $initialVersion = $fullVersion.Split(" ")[2] 60 | $initialVersion -eq "" | Should -BeFalse 61 | 62 | # Run the suc image manually 63 | Log-Info "Executing wins-suc.exe" 64 | $env:CATTLE_WINS_SKIP_BINARY_UPGRADE = "false" 65 | $env:CATTLE_WINS_DEBUG = "true" 66 | Execute-Binary -FilePath "bin\wins-suc.exe" 67 | $LASTEXITCODE | Should -Be -ExpectedValue 0 68 | 69 | # Get the updated version string 70 | $currentVersion = $(c:\Windows\wins.exe --version).Split(" ")[2] 71 | Log-Info "wins.exe version after suc execution: $currentVersion" 72 | $initialVersion -ne $currentVersion | Should -BeTrue 73 | $currentVersion -eq $CIVersion | Should -BeTrue 74 | 75 | # Ensure that the updated file was moved 76 | Test-Path "c:/etc/rancher/wins/wins-$currentVersion.exe" | Should -BeFalse 77 | 78 | Log-Info "Testing updated binaries..." 79 | # Ensure that both paths were updated 80 | $windowsDirVersion = $(c:\Windows\wins.exe --version).Split(" ")[2] 81 | Log-Info "c:\Windows\wins.exe version: $windowsDirVersion" 82 | $usrBinVersion = $(c:\usr\local\bin\wins.exe --version).Split(" ")[2] 83 | Log-Info "c:\usr\local\bin\wins.exe version: $usrBinVersion" 84 | 85 | # Ensure that the version matches what we expect 86 | $windowsDirVersion -eq $CIVersion | Should -BeTrue 87 | $usrBinVersion -eq $CIVersion | Should -BeTrue 88 | 89 | Log-Info "Succesfully Tested Binary Upgrade" 90 | } 91 | } 92 | --------------------------------------------------------------------------------