├── go.mod ├── sockets ├── sockets_unix.go ├── sockets_windows.go ├── unix_socket_test.go ├── tcp_socket.go ├── inmem_socket_test.go ├── unix_socket_unix_test.go ├── unix_socket_unix.go ├── unix_socket.go ├── sockets.go ├── inmem_socket.go ├── unix_socket_windows_test.go └── unix_socket_windows.go ├── doc.go ├── proxy ├── logger.go ├── stub_proxy.go ├── proxy.go ├── tcp_proxy.go ├── udp_proxy.go └── network_proxy_test.go ├── .gitignore ├── tlsconfig ├── certpool.go ├── fixtures │ ├── cert.pem │ ├── cert_of_encrypted_key.pem │ ├── multi.pem │ ├── key.pem │ ├── encrypted_key.pem │ └── generate.go ├── config.go └── config_test.go ├── README.md ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── test.yml ├── go.sum ├── MAINTAINERS ├── nat ├── parse.go ├── sort.go ├── sort_test.go ├── parse_test.go ├── nat.go └── nat_test.go ├── CONTRIBUTING.md └── LICENSE /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/docker/go-connections 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.4.21 7 | golang.org/x/sys v0.1.0 8 | ) 9 | -------------------------------------------------------------------------------- /sockets/sockets_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package sockets 4 | 5 | func configureNpipeTransport(any, string) error { 6 | return ErrProtocolNotAvailable 7 | } 8 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package connections provides libraries to work with network connections. 2 | // This library is divided in several components for specific usage. 3 | package connections 4 | -------------------------------------------------------------------------------- /proxy/logger.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | type logger interface { 4 | Printf(format string, args ...interface{}) 5 | } 6 | 7 | type noopLogger struct{} 8 | 9 | func (l *noopLogger) Printf(_ string, _ ...interface{}) { 10 | // Do nothing :) 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # if you want to ignore files created by your editor/tools, consider using a 2 | # global .gitignore or .git/info/exclude see https://help.github.com/articles/ignoring-files 3 | .* 4 | !.github 5 | !.gitignore 6 | # support running go modules in vendor mode for local development 7 | vendor/ 8 | -------------------------------------------------------------------------------- /tlsconfig/certpool.go: -------------------------------------------------------------------------------- 1 | package tlsconfig 2 | 3 | import ( 4 | "crypto/x509" 5 | "runtime" 6 | ) 7 | 8 | // SystemCertPool returns a copy of the system cert pool, 9 | // returns an error if failed to load or empty pool on windows. 10 | func SystemCertPool() (*x509.CertPool, error) { 11 | certpool, err := x509.SystemCertPool() 12 | if err != nil && runtime.GOOS == "windows" { 13 | return x509.NewCertPool(), nil 14 | } 15 | return certpool, err 16 | } 17 | -------------------------------------------------------------------------------- /sockets/sockets_windows.go: -------------------------------------------------------------------------------- 1 | package sockets 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/Microsoft/go-winio" 9 | ) 10 | 11 | func configureNpipeTransport(tr *http.Transport, addr string) error { 12 | // No need for compression in local communications. 13 | tr.DisableCompression = true 14 | tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { 15 | return winio.DialPipeContext(ctx, addr) 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/docker/go-connections?status.svg)](https://godoc.org/github.com/docker/go-connections) 2 | 3 | # Introduction 4 | 5 | go-connections provides common package to work with network connections. 6 | 7 | ## Usage 8 | 9 | See the [docs in godoc](https://godoc.org/github.com/docker/go-connections) for examples and documentation. 10 | 11 | ## License 12 | 13 | go-connections is licensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for the full license text. 14 | -------------------------------------------------------------------------------- /sockets/unix_socket_test.go: -------------------------------------------------------------------------------- 1 | package sockets 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | func runTest(t *testing.T, path string, l net.Listener, echoStr string) { 10 | go func() { 11 | for { 12 | conn, err := l.Accept() 13 | if err != nil { 14 | return 15 | } 16 | _, _ = conn.Write([]byte(echoStr)) 17 | _ = conn.Close() 18 | } 19 | }() 20 | 21 | conn, err := net.Dial("unix", path) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | buf := make([]byte, 5) 27 | if _, err := conn.Read(buf); err != nil { 28 | t.Fatal(err) 29 | } else if string(buf) != echoStr { 30 | t.Fatal(fmt.Errorf("msg may lost")) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /sockets/tcp_socket.go: -------------------------------------------------------------------------------- 1 | // Package sockets provides helper functions to create and configure Unix or TCP sockets. 2 | package sockets 3 | 4 | import ( 5 | "crypto/tls" 6 | "net" 7 | ) 8 | 9 | // NewTCPSocket creates a TCP socket listener with the specified address and 10 | // the specified tls configuration. If TLSConfig is set, will encapsulate the 11 | // TCP listener inside a TLS one. 12 | func NewTCPSocket(addr string, tlsConfig *tls.Config) (net.Listener, error) { 13 | l, err := net.Listen("tcp", addr) 14 | if err != nil { 15 | return nil, err 16 | } 17 | if tlsConfig != nil { 18 | tlsConfig.NextProtos = []string{"http/1.1"} 19 | l = tls.NewListener(l, tlsConfig) 20 | } 21 | return l, nil 22 | } 23 | -------------------------------------------------------------------------------- /proxy/stub_proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // StubProxy is a proxy that is a stub (does nothing). 8 | type StubProxy struct { 9 | frontendAddr net.Addr 10 | backendAddr net.Addr 11 | } 12 | 13 | // Run does nothing. 14 | func (p *StubProxy) Run() {} 15 | 16 | // Close does nothing. 17 | func (p *StubProxy) Close() {} 18 | 19 | // FrontendAddr returns the frontend address. 20 | func (p *StubProxy) FrontendAddr() net.Addr { return p.frontendAddr } 21 | 22 | // BackendAddr returns the backend address. 23 | func (p *StubProxy) BackendAddr() net.Addr { return p.backendAddr } 24 | 25 | // NewStubProxy creates a new StubProxy 26 | func NewStubProxy(frontendAddr, backendAddr net.Addr) (Proxy, error) { 27 | return &StubProxy{ 28 | frontendAddr: frontendAddr, 29 | backendAddr: backendAddr, 30 | }, nil 31 | } 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 15 | 16 | **- What I did** 17 | 18 | **- How I did it** 19 | 20 | **- How to verify it** 21 | 22 | **- Description for the changelog** 23 | 27 | 28 | 29 | **- A picture of a cute animal (not mandatory but encouraged)** 30 | 31 | -------------------------------------------------------------------------------- /sockets/inmem_socket_test.go: -------------------------------------------------------------------------------- 1 | package sockets 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "testing" 7 | ) 8 | 9 | func TestInmemSocket(t *testing.T) { 10 | l := NewInmemSocket("test", 0) 11 | defer func() { _ = l.Close() }() 12 | go func() { 13 | for { 14 | conn, err := l.Accept() 15 | if err != nil { 16 | return 17 | } 18 | _, _ = conn.Write([]byte("hello")) 19 | _ = conn.Close() 20 | } 21 | }() 22 | 23 | conn, err := l.Dial("test", "test") 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | 28 | buf := make([]byte, 5) 29 | _, err = conn.Read(buf) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | if string(buf) != "hello" { 35 | t.Fatalf("expected `hello`, got %s", string(buf)) 36 | } 37 | 38 | _ = l.Close() 39 | _, err = l.Dial("test", "test") 40 | if !errors.Is(err, net.ErrClosed) { 41 | t.Fatalf(`expected "net.ErrClosed" error, got %[1]v (%[1]T)`, err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro= 2 | github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 7 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 8 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 11 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | -------------------------------------------------------------------------------- /tlsconfig/fixtures/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC1jCCAb6gAwIBAgIDAw0/MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMTBHRl 3 | c3QwHhcNMTYwMzI4MTg0MTQ3WhcNMjcwMzI4MTg0MTQ3WjAPMQ0wCwYDVQQDEwR0 4 | ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1k1NO4wzCpxZ71Bo 5 | SiYSWh8SE9jHtg6lz0QjMQXzFuLhpedjHJYx9fYbD+JVk5vnRbUqNUeZVKAGahfR 6 | 9vhm5I+cm359gYU0gHawLw91oh4JCiwUu77U2obHvtvcXLf6Fb/+MoSA5wH7vbL3 7 | T4vR1+hLt+R+kILAEHq/IlSdLD8CA0iA+ypHfCPOi5F2wVjAyMnQXgVDkAhzefpu 8 | JkhN1yUgb5WK4qoSuOUDUYq/bRosLdHXDJiWRuqaU2zxO5cHVlrNAE5RuspfEzl4 9 | YP6boZTOomLEDbBTSJWgX2/ybvY7o4sCw7KrvyBIqSK9HbfaK1nFMFGoiSH6+1m4 10 | amWKrwIDAQABozswOTAOBgNVHQ8BAf8EBAMCBaAwGQYDVR0lBBIwEAYIKwYBBQUH 11 | AwMGBFUdJQAwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEADuXjLtGk 12 | tU5ql+LFB32Cc2Laa0iO8aqJccOcXYKg4FD0um+1+YQO1CBZZqWjItH4CuJl5+2j 13 | Tc9sFgrIVH5CmvUkOUFPCNDAJtxBvF6RQqRpehjheHDaNsYo9JNKHKEJB6OJrDgy 14 | N5krM5FKyAp/EDTbIrGIZFMdxQGxK5MfpfPkKK44JgOQM3QWeR+LqIpfd34MD1jZ 15 | jjYdl0+quIHiIdFR0a4Uam7o9GfUmcWe1VFthLb5pNhV6t+wyuLyMXVMNacKZSz/ 16 | nOMWVQfgViZk6rHOPSMrFMc7Pp488I907MJKCryd21LcLqMuhb4BpWcJghnY8Lbs 17 | uIPLsUHr3Pfp9Q== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tlsconfig/fixtures/cert_of_encrypted_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC1jCCAb6gAwIBAgIDAw0/MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMTBHRl 3 | c3QwHhcNMTYwNDIyMDQyMjM1WhcNMTgwNDIyMDQyMjM1WjAPMQ0wCwYDVQQDEwR0 4 | ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4GRTos+Ik6kQG7wn 5 | 8E4HqPwgWXbY0T59UQsrbR+YbyxbUKV67Pgl4VImuUmYaism6Tm3EFYzeom5baMc 6 | vW0hC+WbwVr1rq5ddBE8akYhlPY40SxFlh563vOi7lcFGM7xuUbTlhtAhYa5xc5U 7 | thHYa8Mdqc2kMrmU4JBhNHoRk2mnRBo2J2/8RfOfioM6mH0t/MVtB/jSGpcwbbfj 8 | 2twKOpB9CoX57szVo7+DCFHpLxeuop+69REu5Egc2a5BtBuUf0fkUBKuF7yUy2xI 9 | IbgjCiGb3Z+PCIC0CjNt9wExowPAGfxAJ8s1nNlpZav3707VZRtz7Js1skRjm9aU 10 | 8fhYNQIDAQABozswOTAOBgNVHQ8BAf8EBAMCBaAwGQYDVR0lBBIwEAYIKwYBBQUH 11 | AwMGBFUdJQAwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAcKCCV5Os 12 | O2U7Ekp0jzOusV2+ykZzUe4sEds+ikblxK9SHV/pAPIVuAevdyE1LKmJ6ZGgeU2M 13 | 4MC6jC/XTlYNhsYCfKaJn53UscKI2urXFlk1Gv5VQP5EOrMWb76A5uj1nElxKe2C 14 | bMVoUuMwRd9jnz6594D80jGGYpHRaF7yLtGbiflDjB+yv1OU6WnuVNr0nOb9ShR6 15 | WPlrQj5TUSpRHF/oKy9LVWuxYA9aiY1YREDZhhauw9pGAMx1lImfJcJ077MdxN4A 16 | DwKAx3ooajAu1n3McY1oncWW+rWs2Ptvp6lKMGoZ50ElEPCMw4/hPtPMLq/DTWNj 17 | l342KLVWgchlIA== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # go-connections maintainers file 2 | # 3 | # This file describes who runs the docker/go-connections project and how. 4 | # This is a living document - if you see something out of date or missing, speak up! 5 | # 6 | # It is structured to be consumable by both humans and programs. 7 | # To extract its contents programmatically, use any TOML-compliant parser. 8 | # 9 | # This file is compiled into the MAINTAINERS file in docker/opensource. 10 | # 11 | [Org] 12 | [Org."Core maintainers"] 13 | people = [ 14 | "akihirosuda", 15 | "dnephin", 16 | "thajeztah", 17 | "vdemeester", 18 | ] 19 | 20 | [people] 21 | 22 | # A reference list of all people associated with the project. 23 | # All other sections should refer to people by their canonical key 24 | # in the people section. 25 | 26 | # ADD YOURSELF HERE IN ALPHABETICAL ORDER 27 | 28 | [people.akihirosuda] 29 | Name = "Akihiro Suda" 30 | Email = "akihiro.suda.cz@hco.ntt.co.jp" 31 | GitHub = "AkihiroSuda" 32 | 33 | [people.dnephin] 34 | Name = "Daniel Nephin" 35 | Email = "dnephin@gmail.com" 36 | GitHub = "dnephin" 37 | 38 | [people.thajeztah] 39 | Name = "Sebastiaan van Stijn" 40 | Email = "github@gone.nl" 41 | GitHub = "thaJeztah" 42 | 43 | [people.vdemeester] 44 | Name = "Vincent Demeester" 45 | Email = "vincent@sbr.pm" 46 | GitHub = "vdemeester" -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | // Package proxy provides a network Proxy interface and implementations for TCP and UDP. 2 | package proxy 3 | 4 | import ( 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | // Proxy defines the behavior of a proxy. It forwards traffic back and forth 10 | // between two endpoints : the frontend and the backend. 11 | // It can be used to do software port-mapping between two addresses. 12 | // e.g. forward all traffic between the frontend (host) 127.0.0.1:3000 13 | // to the backend (container) at 172.17.42.108:4000. 14 | type Proxy interface { 15 | // Run starts forwarding traffic back and forth between the front 16 | // and back-end addresses. 17 | Run() 18 | // Close stops forwarding traffic and close both ends of the Proxy. 19 | Close() 20 | // FrontendAddr returns the address on which the proxy is listening. 21 | FrontendAddr() net.Addr 22 | // BackendAddr returns the proxied address. 23 | BackendAddr() net.Addr 24 | } 25 | 26 | // NewProxy creates a Proxy according to the specified frontendAddr and backendAddr. 27 | func NewProxy(frontendAddr, backendAddr net.Addr) (Proxy, error) { 28 | switch frontendAddr.(type) { 29 | case *net.UDPAddr: 30 | return NewUDPProxy(frontendAddr.(*net.UDPAddr), backendAddr.(*net.UDPAddr)) 31 | case *net.TCPAddr: 32 | return NewTCPProxy(frontendAddr.(*net.TCPAddr), backendAddr.(*net.TCPAddr)) 33 | default: 34 | panic(fmt.Errorf("unsupported protocol")) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sockets/unix_socket_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package sockets 4 | 5 | import ( 6 | "os" 7 | "syscall" 8 | "testing" 9 | ) 10 | 11 | func TestUnixSocketWithOpts(t *testing.T) { 12 | socketFile, err := os.CreateTemp("", "test*.sock") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | _ = socketFile.Close() 17 | defer func() { _ = os.Remove(socketFile.Name()) }() 18 | 19 | uid, gid := os.Getuid(), os.Getgid() 20 | perms := os.FileMode(0660) 21 | l, err := NewUnixSocketWithOpts(socketFile.Name(), WithChown(uid, gid), WithChmod(perms)) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | p, err := os.Stat(socketFile.Name()) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | if p.Mode().Perm() != perms { 30 | t.Fatalf("unexpected file permissions: expected: %#o, got: %#o", perms, p.Mode().Perm()) 31 | } 32 | if stat, ok := p.Sys().(*syscall.Stat_t); ok { 33 | if stat.Uid != uint32(uid) || stat.Gid != uint32(gid) { 34 | t.Fatalf("unexpected file ownership: expected: %d:%d, got: %d:%d", uid, gid, stat.Uid, stat.Gid) 35 | } 36 | } 37 | 38 | defer func() { _ = l.Close() }() 39 | 40 | echoStr := "hello" 41 | runTest(t, socketFile.Name(), l, echoStr) 42 | } 43 | 44 | // TestNewUnixSocket run under root user. 45 | func TestNewUnixSocket(t *testing.T) { 46 | if os.Getuid() != 0 { 47 | t.Skip("requires root") 48 | } 49 | gid := os.Getgid() 50 | path := "/tmp/test.sock" 51 | echoStr := "hello" 52 | l, err := NewUnixSocket(path, gid) 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | defer func() { _ = l.Close() }() 57 | runTest(t, path, l, echoStr) 58 | } 59 | -------------------------------------------------------------------------------- /tlsconfig/fixtures/multi.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC3DCCAcSgAwIBAgIDAw0/MA0GCSqGSIb3DQEBCwUAMA8xDTALBgNVBAMTBHRl 3 | c3QwHhcNMTYwMzI4MTg0MTQ3WhcNMjcwMzI4MTg0MTQ3WjAPMQ0wCwYDVQQDEwR0 4 | ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArVIJDnNnM1iX7Xj8 5 | bja4WsgHuENRBsBCROTDjQL1w7Ksin2jmCl/D7Gk9ifRJZ/HPE3BKo6B+3CDXygJ 6 | Qvoe8SGWi6ae8lN4VgPoW7xDViAWhVmjIr+dNQXWD0hCq0YZuXyYSi5iXWeRaTvx 7 | 2eoG2VSkNnkc/0weEhX1nBGBscuz1UZqWp53m09eL7otngcNcdjmvLPiw4E3cric 8 | UoLVonzf4ZE84Q7nNmfWfMKh4zJUyn8N766GAAoC6RAKsJ0xSDeRjkzSy7vGJKBv 9 | nTBe6X1xyFZaN0mAjtRkYaxI9ZfI8K41Trhd88s4B4G61p70DY3dMLmuF8wGHVCF 10 | lMMV6wIDAQABo0EwPzAOBgNVHQ8BAf8EBAMCAqQwGQYDVR0lBBIwEAYIKwYBBQUH 11 | AwMGBFUdJQAwEgYDVR0TAQH/BAgwBgEB/wIBATANBgkqhkiG9w0BAQsFAAOCAQEA 12 | LriCH0FTaOFIBl+kxAKjs7puhIZoYLwQ8IReXdEU7kYjPff3X/eiO82A0GwMM9Fp 13 | /RdMlZGDSLyZ1a/gKCz55j9J4MW8ZH7RSEQs3dJQCvEPDO6UdgKy4Ft9yNh/ba1J 14 | 8/n0CqR+0QNov6Qp7eMDkQaDvKgCaABn8at6VLtuifJXFKDGt0LrR7wkQBJ85SZB 15 | 9GdfNSPzEZkb4FQ2gPgAk7ySoQ6Hi6mogEORbtJ7+Xiq57J+cEZQV6TOuwYgBG4e 16 | MW3h37+7V5a/absybik1F/gcx4IbEBd/7an6a+a2l5FeTED5kpzvD4+yrQAoY8lT 17 | gccRdP0O4CsLn7zlLRidPQ== 18 | -----END CERTIFICATE----- 19 | -----BEGIN CERTIFICATE----- 20 | MIIBTzCB9qADAgECAgMDDT8wCgYIKoZIzj0EAwIwDzENMAsGA1UEAxMEdGVzdDAe 21 | Fw0xNjAzMjgxODQxNDdaFw0yNzAzMjgxODQxNDdaMA8xDTALBgNVBAMTBHRlc3Qw 22 | WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQy8xfFkSiJA10EC1MMJzkLgu6csocC 23 | UNyix7zOqijLsASE4an5LQsZ1PuhgVYnL+B9rAcnXgJaLM8YOmLRPqNdo0EwPzAO 24 | BgNVHQ8BAf8EBAMCAqQwGQYDVR0lBBIwEAYIKwYBBQUHAwMGBFUdJQAwEgYDVR0T 25 | AQH/BAgwBgEB/wIBATAKBggqhkjOPQQDAgNIADBFAiEAwUrZY7fHwr4FWONiBJo6 26 | 97V9GAbj70ZJqV5M7rt+hMECIFY66kUrv0sG2vlhicSIGwSOdB3VcijdZSelzLn1 27 | iRk5 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /tlsconfig/fixtures/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA1k1NO4wzCpxZ71BoSiYSWh8SE9jHtg6lz0QjMQXzFuLhpedj 3 | HJYx9fYbD+JVk5vnRbUqNUeZVKAGahfR9vhm5I+cm359gYU0gHawLw91oh4JCiwU 4 | u77U2obHvtvcXLf6Fb/+MoSA5wH7vbL3T4vR1+hLt+R+kILAEHq/IlSdLD8CA0iA 5 | +ypHfCPOi5F2wVjAyMnQXgVDkAhzefpuJkhN1yUgb5WK4qoSuOUDUYq/bRosLdHX 6 | DJiWRuqaU2zxO5cHVlrNAE5RuspfEzl4YP6boZTOomLEDbBTSJWgX2/ybvY7o4sC 7 | w7KrvyBIqSK9HbfaK1nFMFGoiSH6+1m4amWKrwIDAQABAoIBAQC802wj9grbZJzS 8 | A1WBUD6Hbi0tk6uVPR7YnD8t6QIivlL5LgLko2ruQKXjvxiMcai8gT7pp2bxa/d6 9 | 7/Yv2PxAlFH3qOLJhyeVsf7X2JVb/X8VmXXDYAiJbI0AHRX0FJ+lHoDK3nn+En9Q 10 | zSqgyqBhz+s343uptauqWZ2kkE3VNyqlPBhmKc5NcbR7Sgb4nJ3CkNAcxRkl1NeI 11 | BRFdsTUYRNR3Vd++OvOzI4uzZfCIeUVqx+r7/SeLW0UwqeprMm7g+hFQLfH+e9SA 12 | 9lx0EIRoQFwgvKju2eogpSwvkSlObXnESu5OHYtnc+jpsOC0EbQgO0d6CqVZiqjR 13 | 2dRYsZkhAoGBAO69loXSAsyqUj0rT5iq59PuMlBEAlW6hQTfl6c8bnu1JUo2s/CH 14 | OJfswxfHN32qmi99WbK2iLyrnznNYsyPnYKW0ObwuoqAdrlydfu7Fq9HSOACoIvK 15 | jRMOsiJtM3JX2bHHV7yIwJ1+h++o2Ly803j7tKtYsrRQVZiWeTcR2IRZAoGBAOXL 16 | bJFLbAhm3zRqhbiWuORqqyLxrDmIB6RY8vTdX47vwzkFGZJlCuL+vs6877I6eOc9 17 | wjH9qcOiJQJ4DWkAE+VS5PAPoj0UDRw7AkE9v3RwnmxvAfP5rPo5KimYxKq4yX6r 18 | +Qc4ixwftCj0rxFoG4lnipwBFq4NXuHtIhbZXMZHAoGBAOGfatGtV9f0XyRP+jld 19 | yxoO0p3oqAw86dlhNgFmq0NePo+UgxmdsW5i4z1lmJu6z1xyKoMq3q7vwtrtr6GD 20 | WGhB/8tBVgnuvkUkVzw/44Bi7gxGb1OtaQXJra+7ZBN70tCgg9o5o080dWOZPruf 21 | +Hst5eDJQpoGEd7S1lulEeqBAoGBAKAqdIak6izE/wg6wu+Q5lgW3SejCOakoKb1 22 | dIoljkhDZ2/j1RoLoVXsNzRDzlIMnV6X1jYf1ubLqj4ZTUeFTVjGuVl1nCA0TJsD 23 | qiOtFTfkkxeDG/pgaSeTFocdut4/o/nNhep5h8RXeKwfN7LLPH4+FAd+Xr98BEk2 24 | jk8cu6RbAoGAHI9yRXKjlADBZLvxxMGHRfe7eK4PgABmluZLdsXzNmXxybrZDvdC 25 | teipvIUSym7tvdDB6LHXKVp4mYeqHe/ktRatlhbQyPso2VPoMFQyuRBYKKFFAh0V 26 | 3d6EyTRnIxn/NW+XdcCUeufFfd+3BHyux68PyUsTtKRCJYfhExzJf70= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /nat/parse.go: -------------------------------------------------------------------------------- 1 | package nat 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // ParsePortRange parses and validates the specified string as a port range (e.g., "8000-9000"). 11 | func ParsePortRange(ports string) (startPort, endPort uint64, _ error) { 12 | start, end, err := parsePortRange(ports) 13 | return uint64(start), uint64(end), err 14 | } 15 | 16 | // parsePortRange parses and validates the specified string as a port range (e.g., "8000-9000"). 17 | func parsePortRange(ports string) (startPort, endPort int, _ error) { 18 | if ports == "" { 19 | return 0, 0, errors.New("empty string specified for ports") 20 | } 21 | start, end, ok := strings.Cut(ports, "-") 22 | 23 | startPort, err := parsePortNumber(start) 24 | if err != nil { 25 | return 0, 0, fmt.Errorf("invalid start port '%s': %w", start, err) 26 | } 27 | if !ok || start == end { 28 | return startPort, startPort, nil 29 | } 30 | 31 | endPort, err = parsePortNumber(end) 32 | if err != nil { 33 | return 0, 0, fmt.Errorf("invalid end port '%s': %w", end, err) 34 | } 35 | if endPort < startPort { 36 | return 0, 0, errors.New("invalid port range: " + ports) 37 | } 38 | return startPort, endPort, nil 39 | } 40 | 41 | // parsePortNumber parses rawPort into an int, unwrapping strconv errors 42 | // and returning a single "out of range" error for any value outside 0–65535. 43 | func parsePortNumber(rawPort string) (int, error) { 44 | if rawPort == "" { 45 | return 0, errors.New("value is empty") 46 | } 47 | port, err := strconv.ParseInt(rawPort, 10, 0) 48 | if err != nil { 49 | var numErr *strconv.NumError 50 | if errors.As(err, &numErr) { 51 | err = numErr.Err 52 | } 53 | return 0, err 54 | } 55 | if port < 0 || port > 65535 { 56 | return 0, errors.New("value out of range (0–65535)") 57 | } 58 | 59 | return int(port), nil 60 | } 61 | -------------------------------------------------------------------------------- /tlsconfig/fixtures/encrypted_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | Proc-Type: 4,ENCRYPTED 3 | DEK-Info: AES-256-CBC,68ce1d54f187b663e152d9c5dc900fd3 4 | 5 | ZVBeXx7kWiF0yPOORntrN6BsyIJE7krqTVhRfk6GAllaLQv0jvb31XHB1oWOaqnx 6 | tb7kUuoBeQdl1hs/iAnkDMc59WJfEK9A9cAD/SgxTgdENOrzFSRNEfqketLA4eHZ 7 | 2sOLkSfv58HwA0p0gzqSrLQBo/6ZtF/57HxH166PtErPNTS1Usu/f4Oj0UqxTfbZ 8 | B5LHsepyNLt6q/15fcY0TFYJwvgEXa4SridjT+8bTz2T+bx3QFijGnl7EdkTElni 9 | FIwnDjFZaAULqoyUIB1y8guEZVkaWKncxPdRfhId84HklWdrrLtP5D6db1xNNpsp 10 | LzGdciD3phJp6K0hpl+WrhYxuCKURa27tXMCuYOFd1hw/kM29jFbxSIlNBGN4OLL 11 | v4wYrJFM21iWsz9c7Cqw5Yls2Rsx0QrXRFIxwT25z+HNx1fysQxYuxf3r+e2oz8e 12 | 8Os7hvcxG2XDz01/zpx8kzxUcLuh+3o5UOYlo9z6qsjaD5NUXY+X90PUrVO9fk5y 13 | 8o8pnElPnV88Ihrog5YTYy6egiQWHhDk2I4qlYPOBQNKTLg3KulAcmC9vQ8mR5Sy 14 | p3c3MTgh0A3Zk5Dib+sQ0tdbwDcB2JCTqGal1FNEW5Z7qTHA4Bdm2l7hGs8cRpy4 15 | Ehkhv3s5wWmKcbwwlPuJ0UfPeDn6v9qE2/IkOy+jWgTpaFyWtXHc1/XdqMsJ8xN0 16 | thJw/GMtNabB1+zuayJnvmbJd2qW1smsFTHqX3BovXIH4vx1hE2d0lJpEBynk+wr 17 | gpPgrRoEiqsPcsRoVjvKH3qwJLRdcGYhKqhbvRdynlagCLmE8iAI99r82u6t+03h 18 | YNpRbafY4ceAYyK0IlRiJvGkBMfH7bMXcBMmXyQSBF27ZpNidyZSCHrU5xyHqJZO 19 | XWUhl9GHplBfueh5E831S7mDqobd8RqnUvKVygyEOol5VUFDrggTAAKKN9VzM3uT 20 | MaVymt6fA7stzf01fT+Wi7uCm5legTXG3Ca+XxD6TdE0dNzewd5jDsuqwXnt1iC4 21 | slvuLRZeRZDNvBd0G7Ohhp6jb2HHwkv9kQTZ+UEDbR/Gwxty4oT1MnwSE0mi9ZFN 22 | 6PTjrSxpIKe+mAhgzrepLMfATGayYQzucEArPG7Vp+NJva+j6FKloqrzXMjlP0hN 23 | XSBr7AL+j+OR/tzDOoUG3xdsCl/u5hFTpjsW2ti870zoRUcK0fqJ9UIYjh66L7yT 24 | KNkXsC+OcGuGkhtQ0gxx60OI7wp4bh2pKdT6e111/WTvXxVR2C3XhFBLUfNIz/7A 25 | Oj+s0CaV4pBmCjIobLYpxC0ofLplwBLGf9xnsBiQF5dsgKgOhACeDmDMwqAJ3U/t 26 | 54hK/8Yb8W46Tjgbm0Qsj5gFXHofnyqDeQxAjsdCXsdMaPB8nyZpEkuQSEj9HlKW 27 | xIEErVufkvqyrzhX1pxPs+C839Ueyeob6ZWQurqCLTdZh+3bhKcvi5iP+aLLjMWK 28 | JT9tmAuFVkbPerqObVQFbnM4/re33YYD7QXCqta5bxcVeBI8N1HdwMYrDVhXelEx 29 | mqGleUkkDHTWzAa3u1GKOzLXAYnD0TsTwml0+k+Rf0QMBiDJiKujfy7fGqfZF2vR 30 | -----END RSA PRIVATE KEY----- 31 | -------------------------------------------------------------------------------- /sockets/unix_socket_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package sockets 4 | 5 | import ( 6 | "net" 7 | "os" 8 | "syscall" 9 | ) 10 | 11 | // WithChown modifies the socket file's uid and gid 12 | func WithChown(uid, gid int) SockOption { 13 | return func(path string) error { 14 | if err := os.Chown(path, uid, gid); err != nil { 15 | return err 16 | } 17 | return nil 18 | } 19 | } 20 | 21 | // WithChmod modifies socket file's access mode. 22 | func WithChmod(mask os.FileMode) SockOption { 23 | return func(path string) error { 24 | if err := os.Chmod(path, mask); err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | } 30 | 31 | // NewUnixSocket creates a unix socket with the specified path and group. 32 | func NewUnixSocket(path string, gid int) (net.Listener, error) { 33 | return NewUnixSocketWithOpts(path, WithChown(0, gid), WithChmod(0o660)) 34 | } 35 | 36 | func listenUnix(path string) (net.Listener, error) { 37 | // net.Listen does not allow for permissions to be set. As a result, when 38 | // specifying custom permissions ("WithChmod()"), there is a short time 39 | // between creating the socket and applying the permissions, during which 40 | // the socket permissions are Less restrictive than desired. 41 | // 42 | // To work around this limitation of net.Listen(), we temporarily set the 43 | // umask to 0777, which forces the socket to be created with 000 permissions 44 | // (i.e.: no access for anyone). After that, WithChmod() must be used to set 45 | // the desired permissions. 46 | // 47 | // We don't use "defer" here, to reset the umask to its original value as soon 48 | // as possible. Ideally we'd be able to detect if WithChmod() was passed as 49 | // an option, and skip changing umask if default permissions are used. 50 | origUmask := syscall.Umask(0o777) 51 | l, err := net.Listen("unix", path) 52 | syscall.Umask(origUmask) 53 | return l, err 54 | } 55 | -------------------------------------------------------------------------------- /sockets/unix_socket.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package sockets is a simple unix domain socket wrapper. 3 | 4 | # Usage 5 | 6 | For example: 7 | 8 | import( 9 | "fmt" 10 | "net" 11 | "os" 12 | "github.com/docker/go-connections/sockets" 13 | ) 14 | 15 | func main() { 16 | l, err := sockets.NewUnixSocketWithOpts("/path/to/sockets", 17 | sockets.WithChown(0,0),sockets.WithChmod(0660)) 18 | if err != nil { 19 | panic(err) 20 | } 21 | echoStr := "hello" 22 | 23 | go func() { 24 | for { 25 | conn, err := l.Accept() 26 | if err != nil { 27 | return 28 | } 29 | conn.Write([]byte(echoStr)) 30 | conn.Close() 31 | } 32 | }() 33 | 34 | conn, err := net.Dial("unix", path) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | buf := make([]byte, 5) 40 | if _, err := conn.Read(buf); err != nil { 41 | panic(err) 42 | } else if string(buf) != echoStr { 43 | panic(fmt.Errorf("msg may lost")) 44 | } 45 | } 46 | */ 47 | package sockets 48 | 49 | import ( 50 | "net" 51 | "os" 52 | "syscall" 53 | ) 54 | 55 | // SockOption sets up socket file's creating option 56 | type SockOption func(string) error 57 | 58 | // NewUnixSocketWithOpts creates a unix socket with the specified options. 59 | // By default, socket permissions are 0000 (i.e.: no access for anyone); pass 60 | // WithChmod() and WithChown() to set the desired ownership and permissions. 61 | // 62 | // This function temporarily changes the system's "umask" to 0777 to work around 63 | // a race condition between creating the socket and setting its permissions. While 64 | // this should only be for a short duration, it may affect other processes that 65 | // create files/directories during that period. 66 | func NewUnixSocketWithOpts(path string, opts ...SockOption) (net.Listener, error) { 67 | if err := syscall.Unlink(path); err != nil && !os.IsNotExist(err) { 68 | return nil, err 69 | } 70 | 71 | l, err := listenUnix(path) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | for _, op := range opts { 77 | if err := op(path); err != nil { 78 | _ = l.Close() 79 | return nil, err 80 | } 81 | } 82 | 83 | return l, nil 84 | } 85 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Docker 2 | 3 | ### Sign your work 4 | 5 | The sign-off is a simple line at the end of the explanation for the patch. Your 6 | signature certifies that you wrote the patch or otherwise have the right to pass 7 | it on as an open-source patch. The rules are pretty simple: if you can certify 8 | the below (from [developercertificate.org](http://developercertificate.org/)): 9 | 10 | ``` 11 | Developer Certificate of Origin 12 | Version 1.1 13 | 14 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 15 | 660 York Street, Suite 102, 16 | San Francisco, CA 94110 USA 17 | 18 | Everyone is permitted to copy and distribute verbatim copies of this 19 | license document, but changing it is not allowed. 20 | 21 | Developer's Certificate of Origin 1.1 22 | 23 | By making a contribution to this project, I certify that: 24 | 25 | (a) The contribution was created in whole or in part by me and I 26 | have the right to submit it under the open source license 27 | indicated in the file; or 28 | 29 | (b) The contribution is based upon previous work that, to the best 30 | of my knowledge, is covered under an appropriate open source 31 | license and I have the right under that license to submit that 32 | work with modifications, whether created in whole or in part 33 | by me, under the same open source license (unless I am 34 | permitted to submit under a different license), as indicated 35 | in the file; or 36 | 37 | (c) The contribution was provided directly to me by some other 38 | person who certified (a), (b) or (c) and I have not modified 39 | it. 40 | 41 | (d) I understand and agree that this project and the contribution 42 | are public and that a record of the contribution (including all 43 | personal information I submit with it, including my sign-off) is 44 | maintained indefinitely and may be redistributed consistent with 45 | this project or the open source license(s) involved. 46 | ``` 47 | 48 | Then you just add a line to every git commit message: 49 | 50 | Signed-off-by: Joe Smith 51 | 52 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 53 | 54 | If you set your `user.name` and `user.email` git configs, you can sign your 55 | commit automatically with `git commit -s`. 56 | -------------------------------------------------------------------------------- /sockets/sockets.go: -------------------------------------------------------------------------------- 1 | // Package sockets provides helper functions to create and configure Unix or TCP sockets. 2 | package sockets 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | const ( 15 | defaultTimeout = 10 * time.Second 16 | maxUnixSocketPathSize = len(syscall.RawSockaddrUnix{}.Path) 17 | ) 18 | 19 | // ErrProtocolNotAvailable is returned when a given transport protocol is not provided by the operating system. 20 | var ErrProtocolNotAvailable = errors.New("protocol not available") 21 | 22 | // ConfigureTransport configures the specified [http.Transport] according to the specified proto 23 | // and addr. 24 | // 25 | // If the proto is unix (using a unix socket to communicate) or npipe the compression is disabled. 26 | // For other protos, compression is enabled. If you want to manually enable/disable compression, 27 | // make sure you do it _after_ any subsequent calls to ConfigureTransport is made against the same 28 | // [http.Transport]. 29 | func ConfigureTransport(tr *http.Transport, proto, addr string) error { 30 | if tr.MaxIdleConns == 0 { 31 | // prevent long-lived processes from leaking connections 32 | // due to idle connections not being released. 33 | // 34 | // TODO: see if we can also address this from the server side; see: https://github.com/moby/moby/issues/45539 35 | tr.MaxIdleConns = 6 36 | tr.IdleConnTimeout = 30 * time.Second 37 | } 38 | switch proto { 39 | case "unix": 40 | return configureUnixTransport(tr, addr) 41 | case "npipe": 42 | return configureNpipeTransport(tr, addr) 43 | default: 44 | tr.Proxy = http.ProxyFromEnvironment 45 | tr.DisableCompression = false 46 | tr.DialContext = (&net.Dialer{ 47 | Timeout: defaultTimeout, 48 | }).DialContext 49 | } 50 | return nil 51 | } 52 | 53 | func configureUnixTransport(tr *http.Transport, addr string) error { 54 | if len(addr) > maxUnixSocketPathSize { 55 | return fmt.Errorf("unix socket path %q is too long", addr) 56 | } 57 | // No need for compression in local communications. 58 | tr.DisableCompression = true 59 | dialer := &net.Dialer{ 60 | Timeout: defaultTimeout, 61 | } 62 | tr.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) { 63 | return dialer.DialContext(ctx, "unix", addr) 64 | } 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /sockets/inmem_socket.go: -------------------------------------------------------------------------------- 1 | package sockets 2 | 3 | import ( 4 | "net" 5 | "sync" 6 | ) 7 | 8 | // dummyAddr is used to satisfy net.Addr for the in-mem socket 9 | // it is just stored as a string and returns the string for all calls 10 | type dummyAddr string 11 | 12 | // Network returns the addr string, satisfies net.Addr 13 | func (a dummyAddr) Network() string { 14 | return string(a) 15 | } 16 | 17 | // String returns the string form 18 | func (a dummyAddr) String() string { 19 | return string(a) 20 | } 21 | 22 | // InmemSocket implements [net.Listener] using in-memory only connections. 23 | type InmemSocket struct { 24 | chConn chan net.Conn 25 | chClose chan struct{} 26 | addr dummyAddr 27 | mu sync.Mutex 28 | } 29 | 30 | // NewInmemSocket creates an in-memory only [net.Listener]. The addr argument 31 | // can be any string, but is used to satisfy the [net.Listener.Addr] part 32 | // of the [net.Listener] interface 33 | func NewInmemSocket(addr string, bufSize int) *InmemSocket { 34 | return &InmemSocket{ 35 | chConn: make(chan net.Conn, bufSize), 36 | chClose: make(chan struct{}), 37 | addr: dummyAddr(addr), 38 | } 39 | } 40 | 41 | // Addr returns the socket's addr string to satisfy net.Listener 42 | func (s *InmemSocket) Addr() net.Addr { 43 | return s.addr 44 | } 45 | 46 | // Accept implements the Accept method in the Listener interface; it waits 47 | // for the next call and returns a generic Conn. It returns a [net.ErrClosed] 48 | // if the connection is already closed. 49 | func (s *InmemSocket) Accept() (net.Conn, error) { 50 | select { 51 | case conn := <-s.chConn: 52 | return conn, nil 53 | case <-s.chClose: 54 | return nil, net.ErrClosed 55 | } 56 | } 57 | 58 | // Close closes the listener. It will be unavailable for use once closed. 59 | func (s *InmemSocket) Close() error { 60 | s.mu.Lock() 61 | defer s.mu.Unlock() 62 | select { 63 | case <-s.chClose: 64 | default: 65 | close(s.chClose) 66 | } 67 | return nil 68 | } 69 | 70 | // Dial is used to establish a connection with the in-mem server. 71 | // It returns a [net.ErrClosed] if the connection is already closed. 72 | func (s *InmemSocket) Dial(network, addr string) (net.Conn, error) { 73 | srvConn, clientConn := net.Pipe() 74 | select { 75 | case s.chConn <- srvConn: 76 | case <-s.chClose: 77 | return nil, net.ErrClosed 78 | } 79 | 80 | return clientConn, nil 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Default to 'contents: read', which grants actions to read commits. 4 | # 5 | # If any permission is set, any permission not included in the list is 6 | # implicitly set to "none". 7 | # 8 | # see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 9 | permissions: 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | on: 17 | push: 18 | pull_request: 19 | 20 | jobs: 21 | 22 | linux: 23 | name: Test ${{ matrix.platform }} (${{ matrix.go }}) 24 | timeout-minutes: 10 25 | strategy: 26 | fail-fast: false 27 | matrix: 28 | go: ["1.18.x", "oldstable", "stable"] 29 | platform: [ubuntu-24.04] 30 | runs-on: ${{ matrix.platform }} 31 | steps: 32 | - name: Install Go ${{ matrix.go }} 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: ${{ matrix.go }} 36 | - name: Setup IPv6 37 | run: sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=0 net.ipv6.conf.default.disable_ipv6=0 net.ipv6.conf.all.disable_ipv6=0 38 | - name: Checkout code 39 | uses: actions/checkout@v4 40 | - name: Build for ${{ matrix.platform }} 41 | run: go build ./... 42 | - name: Test 43 | run: go test -exec sudo -v ./... 44 | 45 | other: 46 | name: Test ${{ matrix.platform }} (${{ matrix.go }}) 47 | timeout-minutes: 10 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | go: ["1.18.x", "oldstable", "stable"] 52 | platform: [windows-latest, macos-latest] 53 | runs-on: ${{ matrix.platform }} 54 | steps: 55 | - name: Install Go ${{ matrix.go }} 56 | uses: actions/setup-go@v5 57 | with: 58 | go-version: ${{ matrix.go }} 59 | - name: Checkout code 60 | uses: actions/checkout@v4 61 | - name: Build for ${{ matrix.platform }} 62 | run: go build ./... 63 | - name: Test 64 | run: go test -v ./... 65 | 66 | lint: 67 | name: Lint ${{ matrix.platform }} 68 | timeout-minutes: 10 69 | strategy: 70 | fail-fast: false 71 | matrix: 72 | platform: [ubuntu-24.04, windows-latest, macos-latest] 73 | runs-on: ${{ matrix.platform }} 74 | steps: 75 | - uses: actions/checkout@v4 76 | - uses: actions/setup-go@v5 77 | with: 78 | go-version: "stable" 79 | cache: false 80 | - name: golangci-lint 81 | uses: golangci/golangci-lint-action@v7 82 | with: 83 | args: --timeout=5m 84 | -------------------------------------------------------------------------------- /nat/sort.go: -------------------------------------------------------------------------------- 1 | package nat 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | type portSorter struct { 9 | ports []Port 10 | by func(i, j Port) bool 11 | } 12 | 13 | func (s *portSorter) Len() int { 14 | return len(s.ports) 15 | } 16 | 17 | func (s *portSorter) Swap(i, j int) { 18 | s.ports[i], s.ports[j] = s.ports[j], s.ports[i] 19 | } 20 | 21 | func (s *portSorter) Less(i, j int) bool { 22 | ip := s.ports[i] 23 | jp := s.ports[j] 24 | 25 | return s.by(ip, jp) 26 | } 27 | 28 | // Sort sorts a list of ports using the provided predicate 29 | // This function should compare `i` and `j`, returning true if `i` is 30 | // considered to be less than `j` 31 | func Sort(ports []Port, predicate func(i, j Port) bool) { 32 | s := &portSorter{ports, predicate} 33 | sort.Sort(s) 34 | } 35 | 36 | type portMapEntry struct { 37 | port Port 38 | binding *PortBinding 39 | portInt int 40 | portProto string 41 | } 42 | 43 | type portMapSorter []portMapEntry 44 | 45 | func (s portMapSorter) Len() int { return len(s) } 46 | func (s portMapSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 47 | 48 | // Less sorts the port so that the order is: 49 | // 1. port with larger specified bindings 50 | // 2. larger port 51 | // 3. port with tcp protocol 52 | func (s portMapSorter) Less(i, j int) bool { 53 | pi, pj := s[i].portInt, s[j].portInt 54 | var hpi, hpj int 55 | if s[i].binding != nil { 56 | hpi = toInt(s[i].binding.HostPort) 57 | } 58 | if s[j].binding != nil { 59 | hpj = toInt(s[j].binding.HostPort) 60 | } 61 | return hpi > hpj || pi > pj || (pi == pj && strings.EqualFold(s[i].portProto, "tcp")) 62 | } 63 | 64 | // SortPortMap sorts the list of ports and their respected mapping. The ports 65 | // will explicit HostPort will be placed first. 66 | func SortPortMap(ports []Port, bindings map[Port][]PortBinding) { 67 | s := portMapSorter{} 68 | for _, p := range ports { 69 | portInt, portProto := p.Int(), p.Proto() 70 | if binding, ok := bindings[p]; ok && len(binding) > 0 { 71 | for _, b := range binding { 72 | b := b // capture loop variable for go < 1.22 73 | s = append(s, portMapEntry{ 74 | port: p, binding: &b, 75 | portInt: portInt, portProto: portProto, 76 | }) 77 | } 78 | bindings[p] = []PortBinding{} 79 | } else { 80 | s = append(s, portMapEntry{ 81 | port: p, 82 | portInt: portInt, portProto: portProto, 83 | }) 84 | } 85 | } 86 | 87 | sort.Sort(s) 88 | var ( 89 | i int 90 | pm = make(map[Port]struct{}) 91 | ) 92 | // reorder ports 93 | for _, entry := range s { 94 | if _, ok := pm[entry.port]; !ok { 95 | ports[i] = entry.port 96 | pm[entry.port] = struct{}{} 97 | i++ 98 | } 99 | // reorder bindings for this port 100 | if entry.binding != nil { 101 | bindings[entry.port] = append(bindings[entry.port], *entry.binding) 102 | } 103 | } 104 | } 105 | 106 | func toInt(s string) int { 107 | i, _, _ := parsePortRange(s) 108 | return i 109 | } 110 | -------------------------------------------------------------------------------- /nat/sort_test.go: -------------------------------------------------------------------------------- 1 | package nat 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSortUniquePorts(t *testing.T) { 10 | ports := []Port{ 11 | "6379/tcp", 12 | "22/tcp", 13 | } 14 | 15 | Sort(ports, func(ip, jp Port) bool { 16 | return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp") 17 | }) 18 | 19 | first := ports[0] 20 | if string(first) != "22/tcp" { 21 | t.Log(first) 22 | t.Fail() 23 | } 24 | } 25 | 26 | func TestSortSamePortWithDifferentProto(t *testing.T) { 27 | ports := []Port{ 28 | "8888/tcp", 29 | "8888/udp", 30 | "6379/tcp", 31 | "6379/udp", 32 | } 33 | 34 | Sort(ports, func(ip, jp Port) bool { 35 | return ip.Int() < jp.Int() || (ip.Int() == jp.Int() && ip.Proto() == "tcp") 36 | }) 37 | 38 | first := ports[0] 39 | if string(first) != "6379/tcp" { 40 | t.Fail() 41 | } 42 | } 43 | 44 | func TestSortPortMap(t *testing.T) { 45 | ports := []Port{ 46 | "22/tcp", 47 | "22/udp", 48 | "8000/tcp", 49 | "8443/tcp", 50 | "6379/tcp", 51 | "9999/tcp", 52 | } 53 | 54 | portMap := map[Port][]PortBinding{ 55 | "22/tcp": {{}}, 56 | "8000/tcp": {{}}, 57 | "8443/tcp": {}, 58 | "6379/tcp": {{}, {HostIP: "0.0.0.0", HostPort: "32749"}}, 59 | "9999/tcp": {{HostIP: "0.0.0.0", HostPort: "40000"}}, 60 | } 61 | 62 | SortPortMap(ports, portMap) 63 | if !reflect.DeepEqual(ports, []Port{ 64 | "9999/tcp", 65 | "6379/tcp", 66 | "8443/tcp", 67 | "8000/tcp", 68 | "22/tcp", 69 | "22/udp", 70 | }) { 71 | t.Errorf("failed to prioritize port with explicit mappings, got %v", ports) 72 | } 73 | if pm := portMap["6379/tcp"]; !reflect.DeepEqual(pm, []PortBinding{ 74 | {HostIP: "0.0.0.0", HostPort: "32749"}, 75 | {}, 76 | }) { 77 | t.Errorf("failed to prioritize bindings with explicit mappings, got %v", pm) 78 | } 79 | } 80 | 81 | func BenchmarkSortPortMap(b *testing.B) { 82 | const n = 100 83 | ports := make([]Port, 0, n*2) 84 | portMap := make(map[Port][]PortBinding, n*2) 85 | 86 | for i := 0; i < n; i++ { 87 | portNum := 30000 + (i % 50) // force duplicate port numbers 88 | tcp := Port(fmt.Sprintf("%d/tcp", portNum)) 89 | udp := Port(fmt.Sprintf("%d/udp", portNum)) 90 | 91 | ports = append(ports, tcp, udp) 92 | 93 | portMap[tcp] = []PortBinding{ 94 | {HostIP: "127.0.0.2", HostPort: fmt.Sprint(40000 + i)}, 95 | {HostIP: "127.0.0.1", HostPort: fmt.Sprint(40000 + i)}, 96 | } 97 | portMap[udp] = []PortBinding{ 98 | {HostIP: "127.0.0.2", HostPort: fmt.Sprint(40000 + i)}, 99 | {HostIP: "127.0.0.1", HostPort: fmt.Sprint(40000 + i)}, 100 | } 101 | } 102 | 103 | b.ReportAllocs() 104 | b.ResetTimer() 105 | for i := 0; i < b.N; i++ { 106 | portsCopy := make([]Port, len(ports)) 107 | copy(portsCopy, ports) 108 | 109 | bindingsCopy := make(map[Port][]PortBinding, len(portMap)) 110 | for k, v := range portMap { 111 | bindingsCopy[k] = append([]PortBinding(nil), v...) 112 | } 113 | 114 | SortPortMap(portsCopy, bindingsCopy) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /proxy/tcp_proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "syscall" 7 | ) 8 | 9 | // TCPProxy is a proxy for TCP connections. It implements the Proxy interface to 10 | // handle TCP traffic forwarding between the frontend and backend addresses. 11 | type TCPProxy struct { 12 | Logger logger 13 | listener *net.TCPListener 14 | frontendAddr *net.TCPAddr 15 | backendAddr *net.TCPAddr 16 | } 17 | 18 | // NewTCPProxy creates a new TCPProxy. 19 | func NewTCPProxy(frontendAddr, backendAddr *net.TCPAddr, ops ...func(*TCPProxy)) (*TCPProxy, error) { 20 | listener, err := net.ListenTCP("tcp", frontendAddr) 21 | if err != nil { 22 | return nil, err 23 | } 24 | // If the port in frontendAddr was 0 then ListenTCP will have a picked 25 | // a port to listen on, hence the call to Addr to get that actual port: 26 | proxy := &TCPProxy{ 27 | listener: listener, 28 | frontendAddr: listener.Addr().(*net.TCPAddr), 29 | backendAddr: backendAddr, 30 | Logger: &noopLogger{}, 31 | } 32 | 33 | for _, op := range ops { 34 | op(proxy) 35 | } 36 | 37 | return proxy, nil 38 | } 39 | 40 | func (proxy *TCPProxy) clientLoop(client *net.TCPConn, quit chan bool) { 41 | backend, err := net.DialTCP("tcp", nil, proxy.backendAddr) 42 | if err != nil { 43 | proxy.Logger.Printf("Can't forward traffic to backend tcp/%v: %s\n", proxy.backendAddr, err) 44 | _ = client.Close() 45 | return 46 | } 47 | 48 | event := make(chan int64) 49 | broker := func(to, from *net.TCPConn) { 50 | written, err := io.Copy(to, from) 51 | if err != nil { 52 | // If the socket we are writing to is shutdown with 53 | // SHUT_WR, forward it to the other end of the pipe: 54 | if err, ok := err.(*net.OpError); ok && err.Err == syscall.EPIPE { 55 | _ = from.CloseRead() 56 | } 57 | } 58 | _ = to.CloseWrite() 59 | event <- written 60 | } 61 | 62 | go broker(client, backend) 63 | go broker(backend, client) 64 | 65 | var transferred int64 66 | for i := 0; i < 2; i++ { 67 | select { 68 | case written := <-event: 69 | transferred += written 70 | case <-quit: 71 | // Interrupt the two brokers and "join" them. 72 | _ = client.Close() 73 | _ = backend.Close() 74 | for ; i < 2; i++ { 75 | transferred += <-event 76 | } 77 | return 78 | } 79 | } 80 | _ = client.Close() 81 | _ = backend.Close() 82 | } 83 | 84 | // Run starts forwarding the traffic using TCP. 85 | func (proxy *TCPProxy) Run() { 86 | quit := make(chan bool) 87 | defer close(quit) 88 | for { 89 | client, err := proxy.listener.Accept() 90 | if err != nil { 91 | proxy.Logger.Printf("Stopping proxy on tcp/%v for tcp/%v (%s)", proxy.frontendAddr, proxy.backendAddr, err) 92 | return 93 | } 94 | go proxy.clientLoop(client.(*net.TCPConn), quit) 95 | } 96 | } 97 | 98 | // Close stops forwarding the traffic. 99 | func (proxy *TCPProxy) Close() { _ = proxy.listener.Close() } 100 | 101 | // FrontendAddr returns the TCP address on which the proxy is listening. 102 | func (proxy *TCPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } 103 | 104 | // BackendAddr returns the TCP proxied address. 105 | func (proxy *TCPProxy) BackendAddr() net.Addr { return proxy.backendAddr } 106 | -------------------------------------------------------------------------------- /sockets/unix_socket_windows_test.go: -------------------------------------------------------------------------------- 1 | package sockets 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestGetSecurityDescriptor(t *testing.T) { 10 | t.Run("Default", func(t *testing.T) { 11 | sddl, err := getSecurityDescriptor() 12 | if err != nil { 13 | t.Error(err) 14 | } 15 | expected := BasePermissions 16 | if sddl != expected { 17 | t.Errorf("expected: %s, got: %s", expected, sddl) 18 | } 19 | }) 20 | t.Run("Users", func(t *testing.T) { 21 | const name = "Users" // for testing, should always be available 22 | sddl, err := getSecurityDescriptor(name) 23 | if err != nil { 24 | t.Error(err) 25 | } 26 | // FIXME(thaJeztah): this may not be a reproducible SID; probably should do some fuzzy matching. 27 | const expected = "D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GRGW;;;S-1-5-32-545)" 28 | if sddl != expected { 29 | t.Errorf("expected: %s, got: %s", expected, sddl) 30 | } 31 | }) 32 | 33 | // TODO(thaJeztah): should this fail on duplicate users? 34 | t.Run("Users twice", func(t *testing.T) { 35 | const name = "Users" // for testing, should always be available 36 | sddl, err := getSecurityDescriptor(name, name) 37 | if err != nil { 38 | t.Error(err) 39 | } 40 | // FIXME(thaJeztah): this may not be a reproducible SID; probably should do some fuzzy matching. 41 | const expected = "D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GRGW;;;S-1-5-32-545)(A;;GRGW;;;S-1-5-32-545)" 42 | if sddl != expected { 43 | t.Errorf("expected: %s, got: %s", expected, sddl) 44 | } 45 | }) 46 | t.Run("NoSuchUserOrGroup", func(t *testing.T) { 47 | const name = "NoSuchUserOrGroup" // non-existing user or group 48 | sddl, err := getSecurityDescriptor(name) 49 | if sddl != "" { 50 | t.Errorf("expected an empty sddl, got: %s", sddl) 51 | } 52 | if err == nil { 53 | t.Error("expected error") 54 | } 55 | 56 | const expected = "looking up SID: lookup account NoSuchUserOrGroup: not found" 57 | if errMsg := err.Error(); errMsg != expected { 58 | t.Errorf("expected: %s, got: %s", expected, errMsg) 59 | } 60 | }) 61 | } 62 | 63 | func TestUnixSocketWithOpts(t *testing.T) { 64 | socketFile, err := os.CreateTemp("", "test*.sock") 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | _ = socketFile.Close() 69 | defer func() { _ = os.Remove(socketFile.Name()) }() 70 | 71 | l, err := NewUnixSocketWithOpts(socketFile.Name()) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | defer func() { _ = l.Close() }() 76 | 77 | echoStr := "hello" 78 | runTest(t, socketFile.Name(), l, echoStr) 79 | } 80 | 81 | func TestNewUnixSocket(t *testing.T) { 82 | group := "Users" // for testing, should always be available 83 | socketPath := filepath.Join(os.TempDir(), "test.sock") 84 | defer func() { _ = os.Remove(socketPath) }() 85 | t.Logf("socketPath: %s, path length: %d", socketPath, len(socketPath)) 86 | 87 | l, err := NewUnixSocket(socketPath, []string{group}) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | defer func() { _ = l.Close() }() 92 | runTest(t, socketPath, l, "hello") 93 | } 94 | 95 | func TestNewUnixSocketUnknownGroup(t *testing.T) { 96 | group := "NoSuchUserOrGroup" 97 | socketPath := filepath.Join(os.TempDir(), "fail.sock") 98 | _, err := NewUnixSocket(socketPath, []string{group}) 99 | _ = os.Remove(socketPath) 100 | if err == nil { 101 | t.Errorf("expected error, got nil") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tlsconfig/fixtures/generate.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/x509" 10 | "crypto/x509/pkix" 11 | "encoding/pem" 12 | "fmt" 13 | "io" 14 | "log" 15 | "math/big" 16 | "os" 17 | "time" 18 | ) 19 | 20 | //go:generate go run ${GOFILE} 21 | 22 | var certTemplate = x509.Certificate{ 23 | SerialNumber: big.NewInt(199999), 24 | Subject: pkix.Name{ 25 | CommonName: "test", 26 | }, 27 | NotBefore: time.Now().AddDate(-1, 1, 1), 28 | NotAfter: time.Now().AddDate(1, 1, 1), 29 | 30 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 31 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning, x509.ExtKeyUsageAny}, 32 | 33 | BasicConstraintsValid: true, 34 | } 35 | 36 | func generateCertificate(signer crypto.Signer, out io.Writer, isCA bool) error { 37 | template := certTemplate 38 | template.IsCA = isCA 39 | if isCA { 40 | template.KeyUsage = template.KeyUsage | x509.KeyUsageCertSign 41 | template.MaxPathLen = 1 42 | } 43 | derBytes, err := x509.CreateCertificate(rand.Reader, &template, &certTemplate, signer.Public(), signer) 44 | if err != nil { 45 | return fmt.Errorf("unable to generate a certificate: %w", err) 46 | } 47 | 48 | if err = pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil { 49 | return fmt.Errorf("unable to write cert to file: %w", err) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // generates a multiple-certificate CA file with both RSA and ECDSA certs and 56 | // returns the filename so that cleanup can be deferred. 57 | func generateMultiCert() error { 58 | certOut, err := os.Create("multi.pem") 59 | if err != nil { 60 | return fmt.Errorf("unable to create file to write multi-cert to: %w", err) 61 | } 62 | defer func() { _ = certOut.Close() }() 63 | 64 | rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) 65 | if err != nil { 66 | return fmt.Errorf("unable to generate RSA key for multi-cert: %w", err) 67 | } 68 | ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 69 | if err != nil { 70 | return fmt.Errorf("unable to generate ECDSA key for multi-cert: %w", err) 71 | } 72 | 73 | for _, signer := range []crypto.Signer{rsaKey, ecKey} { 74 | if err := generateCertificate(signer, certOut, true); err != nil { 75 | return err 76 | } 77 | } 78 | 79 | return nil 80 | } 81 | 82 | func generateCertAndKey() error { 83 | rsaKey, err := rsa.GenerateKey(rand.Reader, 2048) 84 | if err != nil { 85 | return fmt.Errorf("unable to generate RSA key: %w", err) 86 | 87 | } 88 | keyBytes := x509.MarshalPKCS1PrivateKey(rsaKey) 89 | 90 | keyOut, err := os.Create("key.pem") 91 | if err != nil { 92 | return fmt.Errorf("unable to create file to write key to: %w", err) 93 | } 94 | defer func() { _ = keyOut.Close() }() 95 | 96 | if err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes}); err != nil { 97 | return fmt.Errorf("unable to write key to file: %w", err) 98 | } 99 | 100 | certOut, err := os.Create("cert.pem") 101 | if err != nil { 102 | return fmt.Errorf("to create file to write cert to: %w", err) 103 | } 104 | defer func() { _ = certOut.Close() }() 105 | 106 | return generateCertificate(rsaKey, certOut, false) 107 | } 108 | 109 | func main() { 110 | if err := generateCertAndKey(); err != nil { 111 | log.Fatal(err) 112 | } 113 | if err := generateMultiCert(); err != nil { 114 | log.Fatal(err) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /nat/parse_test.go: -------------------------------------------------------------------------------- 1 | package nat 2 | 3 | import "testing" 4 | 5 | func TestParsePortRange(t *testing.T) { 6 | tests := []struct { 7 | doc string 8 | input string 9 | expBegin uint64 10 | expEnd uint64 11 | expErr string 12 | }{ 13 | { 14 | doc: "empty value", 15 | expErr: `empty string specified for ports`, 16 | }, 17 | { 18 | doc: "single port", 19 | input: "1234", 20 | expBegin: 1234, 21 | expEnd: 1234, 22 | }, 23 | { 24 | doc: "single port range", 25 | input: "1234-1234", 26 | expBegin: 1234, 27 | expEnd: 1234, 28 | }, 29 | { 30 | doc: "two port range", 31 | input: "1234-1235", 32 | expBegin: 1234, 33 | expEnd: 1235, 34 | }, 35 | { 36 | doc: "large range", 37 | input: "8000-9000", 38 | expBegin: 8000, 39 | expEnd: 9000, 40 | }, 41 | { 42 | doc: "zero port", 43 | input: "0", 44 | }, 45 | { 46 | doc: "zero range", 47 | input: "0-0", 48 | }, 49 | // invalid cases 50 | { 51 | doc: "non-numeric port", 52 | input: "asdf", 53 | expErr: `invalid start port 'asdf': invalid syntax`, 54 | }, 55 | { 56 | doc: "reversed range", 57 | input: "9000-8000", 58 | expErr: `invalid port range: 9000-8000`, 59 | }, 60 | { 61 | doc: "range missing end", 62 | input: "8000-", 63 | expErr: `invalid end port '': value is empty`, 64 | }, 65 | { 66 | doc: "range missing start", 67 | input: "-9000", 68 | expErr: `invalid start port '': value is empty`, 69 | }, 70 | { 71 | doc: "invalid range end", 72 | input: "8000-a", 73 | expErr: `invalid end port 'a': invalid syntax`, 74 | }, 75 | { 76 | doc: "invalid range end port", 77 | input: "8000-9000a", 78 | expErr: `invalid end port '9000a': invalid syntax`, 79 | }, 80 | { 81 | doc: "range range start", 82 | input: "a-9000", 83 | expErr: `invalid start port 'a': invalid syntax`, 84 | }, 85 | { 86 | doc: "range range start port", 87 | input: "8000a-9000", 88 | expErr: `invalid start port '8000a': invalid syntax`, 89 | }, 90 | { 91 | doc: "range with trailing hyphen", 92 | input: "-8000-", 93 | expErr: `invalid start port '': value is empty`, 94 | }, 95 | { 96 | doc: "range without ports", 97 | input: "-", 98 | expErr: `invalid start port '': value is empty`, 99 | }, 100 | } 101 | 102 | for _, tc := range tests { 103 | t.Run(tc.doc, func(t *testing.T) { 104 | begin, end, err := ParsePortRange(tc.input) 105 | if tc.expErr == "" { 106 | if err != nil { 107 | t.Error(err) 108 | } 109 | } else { 110 | if err == nil || err.Error() != tc.expErr { 111 | t.Errorf("expected error '%s', got '%v'", tc.expErr, err) 112 | } 113 | } 114 | if begin != tc.expBegin { 115 | t.Errorf("expected begin %d, got %d", tc.expBegin, begin) 116 | } 117 | if end != tc.expEnd { 118 | t.Errorf("expected end %d, got %d", tc.expEnd, end) 119 | } 120 | }) 121 | } 122 | } 123 | 124 | func TestParsePortNumber(t *testing.T) { 125 | tests := []struct { 126 | doc string 127 | input string 128 | exp int 129 | expErr string 130 | }{ 131 | { 132 | doc: "empty string", 133 | input: "", 134 | expErr: "value is empty", 135 | }, 136 | { 137 | doc: "whitespace only", 138 | input: " ", 139 | expErr: "invalid syntax", 140 | }, 141 | { 142 | doc: "single valid port", 143 | input: "1234", 144 | exp: 1234, 145 | }, 146 | { 147 | doc: "zero port", 148 | input: "0", 149 | exp: 0, 150 | }, 151 | { 152 | doc: "max valid port", 153 | input: "65535", 154 | exp: 65535, 155 | }, 156 | { 157 | doc: "leading/trailing spaces", 158 | input: " 42 ", 159 | expErr: "invalid syntax", 160 | }, 161 | { 162 | doc: "negative port", 163 | input: "-1", 164 | expErr: "value out of range (0–65535)", 165 | }, 166 | { 167 | doc: "too large port", 168 | input: "70000", 169 | expErr: "value out of range (0–65535)", 170 | }, 171 | { 172 | doc: "non-numeric", 173 | input: "foo", 174 | expErr: "invalid syntax", 175 | }, 176 | { 177 | doc: "trailing garbage", 178 | input: "1234abc", 179 | expErr: "invalid syntax", 180 | }, 181 | } 182 | 183 | for _, tc := range tests { 184 | t.Run(tc.doc, func(t *testing.T) { 185 | got, err := parsePortNumber(tc.input) 186 | 187 | if tc.expErr == "" { 188 | if err != nil { 189 | t.Fatalf("unexpected error: %v", err) 190 | } 191 | if got != tc.exp { 192 | t.Errorf("expected %d, got %d", tc.exp, got) 193 | } 194 | } else { 195 | if err == nil { 196 | t.Fatalf("expected error %q, got nil", tc.expErr) 197 | } 198 | if err.Error() != tc.expErr { 199 | t.Errorf("expected error %q, got %q", tc.expErr, err.Error()) 200 | } 201 | } 202 | }) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /proxy/udp_proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "net" 7 | "sync" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | const ( 13 | // UDPConnTrackTimeout is the timeout used for UDP connection tracking 14 | UDPConnTrackTimeout = 90 * time.Second 15 | // UDPBufSize is the buffer size for the UDP proxy 16 | UDPBufSize = 65507 17 | ) 18 | 19 | // A net.Addr where the IP is split into two fields so you can use it as a key 20 | // in a map: 21 | type connTrackKey struct { 22 | IPHigh uint64 23 | IPLow uint64 24 | Port int 25 | } 26 | 27 | func newConnTrackKey(addr *net.UDPAddr) *connTrackKey { 28 | if len(addr.IP) == net.IPv4len { 29 | return &connTrackKey{ 30 | IPHigh: 0, 31 | IPLow: uint64(binary.BigEndian.Uint32(addr.IP)), 32 | Port: addr.Port, 33 | } 34 | } 35 | return &connTrackKey{ 36 | IPHigh: binary.BigEndian.Uint64(addr.IP[:8]), 37 | IPLow: binary.BigEndian.Uint64(addr.IP[8:]), 38 | Port: addr.Port, 39 | } 40 | } 41 | 42 | type connTrackMap map[connTrackKey]*net.UDPConn 43 | 44 | // UDPProxy is proxy for which handles UDP datagrams. It implements the Proxy 45 | // interface to handle UDP traffic forwarding between the frontend and backend 46 | // addresses. 47 | type UDPProxy struct { 48 | Logger logger 49 | listener *net.UDPConn 50 | frontendAddr *net.UDPAddr 51 | backendAddr *net.UDPAddr 52 | connTrackTable connTrackMap 53 | connTrackLock sync.Mutex 54 | } 55 | 56 | // NewUDPProxy creates a new UDPProxy. 57 | func NewUDPProxy(frontendAddr, backendAddr *net.UDPAddr, ops ...func(*UDPProxy)) (*UDPProxy, error) { 58 | listener, err := net.ListenUDP("udp", frontendAddr) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | proxy := &UDPProxy{ 64 | listener: listener, 65 | frontendAddr: listener.LocalAddr().(*net.UDPAddr), 66 | backendAddr: backendAddr, 67 | connTrackTable: make(connTrackMap), 68 | Logger: &noopLogger{}, 69 | } 70 | 71 | for _, op := range ops { 72 | op(proxy) 73 | } 74 | 75 | return proxy, nil 76 | } 77 | 78 | func (proxy *UDPProxy) replyLoop(proxyConn *net.UDPConn, clientAddr *net.UDPAddr, clientKey *connTrackKey) { 79 | defer func() { 80 | proxy.connTrackLock.Lock() 81 | delete(proxy.connTrackTable, *clientKey) 82 | proxy.connTrackLock.Unlock() 83 | _ = proxyConn.Close() 84 | }() 85 | 86 | readBuf := make([]byte, UDPBufSize) 87 | for { 88 | _ = proxyConn.SetReadDeadline(time.Now().Add(UDPConnTrackTimeout)) 89 | again: 90 | read, err := proxyConn.Read(readBuf) 91 | if err != nil { 92 | if err, ok := err.(*net.OpError); ok && err.Err == syscall.ECONNREFUSED { 93 | // This will happen if the last write failed 94 | // (e.g: nothing is actually listening on the 95 | // proxied port on the container), ignore it 96 | // and continue until UDPConnTrackTimeout 97 | // expires: 98 | goto again 99 | } 100 | return 101 | } 102 | for i := 0; i != read; { 103 | written, err := proxy.listener.WriteToUDP(readBuf[i:read], clientAddr) 104 | if err != nil { 105 | return 106 | } 107 | i += written 108 | } 109 | } 110 | } 111 | 112 | // Run starts forwarding the traffic using UDP. 113 | func (proxy *UDPProxy) Run() { 114 | readBuf := make([]byte, UDPBufSize) 115 | for { 116 | read, from, err := proxy.listener.ReadFromUDP(readBuf) 117 | if err != nil { 118 | // NOTE: Apparently ReadFrom doesn't return ECONNREFUSED like 119 | // Read does (see comment in [UDPProxy.replyLoop]). 120 | if !errors.Is(err, net.ErrClosed) { 121 | proxy.Logger.Printf("Stopping proxy on udp/%v for udp/%v (%s)", proxy.frontendAddr, proxy.backendAddr, err) 122 | } 123 | break 124 | } 125 | 126 | fromKey := newConnTrackKey(from) 127 | proxy.connTrackLock.Lock() 128 | proxyConn, hit := proxy.connTrackTable[*fromKey] 129 | if !hit { 130 | proxyConn, err = net.DialUDP("udp", nil, proxy.backendAddr) 131 | if err != nil { 132 | proxy.Logger.Printf("Can't proxy a datagram to udp/%s: %s\n", proxy.backendAddr, err) 133 | proxy.connTrackLock.Unlock() 134 | continue 135 | } 136 | proxy.connTrackTable[*fromKey] = proxyConn 137 | go proxy.replyLoop(proxyConn, from, fromKey) 138 | } 139 | proxy.connTrackLock.Unlock() 140 | for i := 0; i != read; { 141 | written, err := proxyConn.Write(readBuf[i:read]) 142 | if err != nil { 143 | proxy.Logger.Printf("Can't proxy a datagram to udp/%s: %s\n", proxy.backendAddr, err) 144 | break 145 | } 146 | i += written 147 | } 148 | } 149 | } 150 | 151 | // Close stops forwarding the traffic. 152 | func (proxy *UDPProxy) Close() { 153 | _ = proxy.listener.Close() 154 | proxy.connTrackLock.Lock() 155 | defer proxy.connTrackLock.Unlock() 156 | for _, conn := range proxy.connTrackTable { 157 | _ = conn.Close() 158 | } 159 | } 160 | 161 | // FrontendAddr returns the UDP address on which the proxy is listening. 162 | func (proxy *UDPProxy) FrontendAddr() net.Addr { return proxy.frontendAddr } 163 | 164 | // BackendAddr returns the proxied UDP address. 165 | func (proxy *UDPProxy) BackendAddr() net.Addr { return proxy.backendAddr } 166 | -------------------------------------------------------------------------------- /sockets/unix_socket_windows.go: -------------------------------------------------------------------------------- 1 | package sockets 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | "github.com/Microsoft/go-winio" 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | // BasePermissions defines the default DACL, which allows Administrators 14 | // and LocalSystem full access (similar to defaults used in [moby]); 15 | // 16 | // - D:P: DACL without inheritance (protected, (P)). 17 | // - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA). 18 | // - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY). 19 | // - Any other user is denied access. 20 | // 21 | // [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59 22 | const BasePermissions = "D:P(A;;GA;;;BA)(A;;GA;;;SY)" 23 | 24 | // WithBasePermissions sets a default DACL, which allows Administrators 25 | // and LocalSystem full access (similar to defaults used in [moby]); 26 | // 27 | // - D:P: DACL without inheritance (protected, (P)). 28 | // - (A;;GA;;;BA): Allow full access (GA) for built-in Administrators (BA). 29 | // - (A;;GA;;;SY); Allow full access (GA) for LocalSystem (SY). 30 | // - Any other user is denied access. 31 | // 32 | // [moby]: https://github.com/moby/moby/blob/6b45c76a233b1b8b56465f76c21c09fd7920e82d/daemon/listeners/listeners_windows.go#L53-L59 33 | func WithBasePermissions() SockOption { 34 | return withSDDL(BasePermissions) 35 | } 36 | 37 | // WithAdditionalUsersAndGroups modifies the socket file's DACL to grant 38 | // access to additional users and groups. 39 | // 40 | // It sets [BasePermissions] on the socket path and grants the given additional 41 | // users and groups to generic read (GR) and write (GW) access. It returns 42 | // an error if no groups were given, when failing to resolve any of the 43 | // additional users and groups, or when failing to apply the ACL. 44 | func WithAdditionalUsersAndGroups(additionalUsersAndGroups []string) SockOption { 45 | return func(path string) error { 46 | if len(additionalUsersAndGroups) == 0 { 47 | return errors.New("no additional users specified") 48 | } 49 | sd, err := getSecurityDescriptor(additionalUsersAndGroups...) 50 | if err != nil { 51 | return fmt.Errorf("looking up SID: %w", err) 52 | } 53 | return withSDDL(sd)(path) 54 | } 55 | } 56 | 57 | // withSDDL applies the given SDDL to the socket. It returns an error 58 | // when failing parse the SDDL, or if the DACL was defaulted. 59 | // 60 | // TODO(thaJeztah); this is not exported yet, as some of the checks may need review if they're not too opinionated. 61 | func withSDDL(sddl string) SockOption { 62 | return func(path string) error { 63 | sd, err := windows.SecurityDescriptorFromString(sddl) 64 | if err != nil { 65 | return fmt.Errorf("parsing SDDL: %w", err) 66 | } 67 | dacl, defaulted, err := sd.DACL() 68 | if err != nil { 69 | return fmt.Errorf("extracting DACL: %w", err) 70 | } 71 | if dacl == nil || defaulted { 72 | // should never be hit with our [DefaultPermissions], 73 | // as it contains "D:" and "P" (protected, don't inherit). 74 | return errors.New("no DACL found in security descriptor or defaulted") 75 | } 76 | return windows.SetNamedSecurityInfo( 77 | path, 78 | windows.SE_FILE_OBJECT, 79 | windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, 80 | nil, // do not change the owner 81 | nil, // do not change the owner 82 | dacl, 83 | nil, 84 | ) 85 | } 86 | } 87 | 88 | // NewUnixSocket creates a new unix socket. 89 | // 90 | // It sets [BasePermissions] on the socket path and grants the given additional 91 | // users and groups to generic read (GR) and write (GW) access. It returns 92 | // an error when failing to resolve any of the additional users and groups, 93 | // or when failing to apply the ACL. 94 | func NewUnixSocket(path string, additionalUsersAndGroups []string) (net.Listener, error) { 95 | var opts []SockOption 96 | if len(additionalUsersAndGroups) > 0 { 97 | opts = append(opts, WithAdditionalUsersAndGroups(additionalUsersAndGroups)) 98 | } else { 99 | opts = append(opts, WithBasePermissions()) 100 | } 101 | return NewUnixSocketWithOpts(path, opts...) 102 | } 103 | 104 | // getSecurityDescriptor returns the DACL for the Unix socket. 105 | // 106 | // By default, it grants [BasePermissions], but allows for additional 107 | // users and groups to get generic read (GR) and write (GW) access. It 108 | // returns an error when failing to resolve any of the additional users 109 | // and groups. 110 | func getSecurityDescriptor(additionalUsersAndGroups ...string) (string, error) { 111 | sddl := BasePermissions 112 | 113 | // Grant generic read (GR) and write (GW) access to whatever 114 | // additional users or groups were specified. 115 | // 116 | // TODO(thaJeztah): should we fail on, or remove duplicates? 117 | for _, g := range additionalUsersAndGroups { 118 | sid, err := winio.LookupSidByName(strings.TrimSpace(g)) 119 | if err != nil { 120 | return "", fmt.Errorf("looking up SID: %w", err) 121 | } 122 | sddl += fmt.Sprintf("(A;;GRGW;;;%s)", sid) 123 | } 124 | return sddl, nil 125 | } 126 | 127 | func listenUnix(path string) (net.Listener, error) { 128 | return net.Listen("unix", path) 129 | } 130 | -------------------------------------------------------------------------------- /proxy/network_proxy_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | "runtime" 9 | "strings" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | var ( 15 | testBuf = []byte("Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo") 16 | testBufSize = len(testBuf) 17 | ) 18 | 19 | type EchoServer interface { 20 | Run() 21 | Close() 22 | LocalAddr() net.Addr 23 | } 24 | 25 | type TCPEchoServer struct { 26 | listener net.Listener 27 | testCtx *testing.T 28 | } 29 | 30 | type UDPEchoServer struct { 31 | conn net.PacketConn 32 | testCtx *testing.T 33 | } 34 | 35 | func NewEchoServer(t *testing.T, proto, address string) EchoServer { 36 | var server EchoServer 37 | if strings.HasPrefix(proto, "tcp") { 38 | listener, err := net.Listen(proto, address) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | server = &TCPEchoServer{listener: listener, testCtx: t} 43 | } else { 44 | socket, err := net.ListenPacket(proto, address) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | server = &UDPEchoServer{conn: socket, testCtx: t} 49 | } 50 | return server 51 | } 52 | 53 | func (server *TCPEchoServer) Run() { 54 | go func() { 55 | for { 56 | client, err := server.listener.Accept() 57 | if err != nil { 58 | return 59 | } 60 | go func(client net.Conn) { 61 | if _, err := io.Copy(client, client); err != nil { 62 | server.testCtx.Logf("can't echo to the client: %v\n", err.Error()) 63 | } 64 | _ = client.Close() 65 | }(client) 66 | } 67 | }() 68 | } 69 | 70 | func (server *TCPEchoServer) LocalAddr() net.Addr { return server.listener.Addr() } 71 | func (server *TCPEchoServer) Close() { _ = server.listener.Close() } 72 | 73 | func (server *UDPEchoServer) Run() { 74 | go func() { 75 | readBuf := make([]byte, 1024) 76 | for { 77 | read, from, err := server.conn.ReadFrom(readBuf) 78 | if err != nil { 79 | return 80 | } 81 | for i := 0; i != read; { 82 | written, err := server.conn.WriteTo(readBuf[i:read], from) 83 | if err != nil { 84 | break 85 | } 86 | i += written 87 | } 88 | } 89 | }() 90 | } 91 | 92 | func (server *UDPEchoServer) LocalAddr() net.Addr { return server.conn.LocalAddr() } 93 | func (server *UDPEchoServer) Close() { _ = server.conn.Close() } 94 | 95 | func testProxyAt(t *testing.T, proto string, proxy Proxy, addr string) { 96 | defer proxy.Close() 97 | go proxy.Run() 98 | client, err := net.Dial(proto, addr) 99 | if err != nil { 100 | t.Fatalf("Can't connect to the proxy: %v", err) 101 | } 102 | defer func() { _ = client.Close() }() 103 | _ = client.SetDeadline(time.Now().Add(10 * time.Second)) 104 | if _, err = client.Write(testBuf); err != nil { 105 | t.Fatal(err) 106 | } 107 | recvBuf := make([]byte, testBufSize) 108 | if _, err = client.Read(recvBuf); err != nil { 109 | t.Fatal(err) 110 | } 111 | if !bytes.Equal(testBuf, recvBuf) { 112 | t.Fatal(fmt.Errorf("expected [%v] but got [%v]", testBuf, recvBuf)) 113 | } 114 | } 115 | 116 | func testProxy(t *testing.T, proto string, proxy Proxy) { 117 | testProxyAt(t, proto, proxy, proxy.FrontendAddr().String()) 118 | } 119 | 120 | func TestTCP4Proxy(t *testing.T) { 121 | backend := NewEchoServer(t, "tcp", "127.0.0.1:0") 122 | defer backend.Close() 123 | backend.Run() 124 | frontendAddr := &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} 125 | proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | testProxy(t, "tcp", proxy) 130 | } 131 | 132 | func TestTCP6Proxy(t *testing.T) { 133 | backend := NewEchoServer(t, "tcp", "[::1]:0") 134 | defer backend.Close() 135 | backend.Run() 136 | frontendAddr := &net.TCPAddr{IP: net.IPv6loopback, Port: 0} 137 | proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | testProxy(t, "tcp", proxy) 142 | } 143 | 144 | func TestTCPDualStackProxy(t *testing.T) { 145 | // If I understand `godoc -src net favoriteAddrFamily` (used by the 146 | // net.Listen* functions) correctly this should work, but it doesn't. 147 | t.Skip("No support for dual stack yet") 148 | backend := NewEchoServer(t, "tcp", "[::1]:0") 149 | defer backend.Close() 150 | backend.Run() 151 | frontendAddr := &net.TCPAddr{IP: net.IPv6loopback, Port: 0} 152 | proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | ipv4ProxyAddr := &net.TCPAddr{ 157 | IP: net.IPv4(127, 0, 0, 1), 158 | Port: proxy.FrontendAddr().(*net.TCPAddr).Port, 159 | } 160 | testProxyAt(t, "tcp", proxy, ipv4ProxyAddr.String()) 161 | } 162 | 163 | func TestUDP4Proxy(t *testing.T) { 164 | backend := NewEchoServer(t, "udp", "127.0.0.1:0") 165 | defer backend.Close() 166 | backend.Run() 167 | frontendAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} 168 | proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) 169 | if err != nil { 170 | t.Fatal(err) 171 | } 172 | testProxy(t, "udp", proxy) 173 | } 174 | 175 | func TestUDP6Proxy(t *testing.T) { 176 | backend := NewEchoServer(t, "udp", "[::1]:0") 177 | defer backend.Close() 178 | backend.Run() 179 | frontendAddr := &net.UDPAddr{IP: net.IPv6loopback, Port: 0} 180 | proxy, err := NewProxy(frontendAddr, backend.LocalAddr()) 181 | if err != nil { 182 | t.Fatal(err) 183 | } 184 | testProxy(t, "udp", proxy) 185 | } 186 | 187 | func TestUDPWriteError(t *testing.T) { 188 | if runtime.GOOS == "darwin" { 189 | t.Skip("FIXME: doesn't pass on macOS") 190 | } 191 | frontendAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} 192 | // Hopefully, this port will be free: */ 193 | backendAddr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 25587} 194 | proxy, err := NewProxy(frontendAddr, backendAddr) 195 | if err != nil { 196 | t.Fatal(err) 197 | } 198 | defer proxy.Close() 199 | go proxy.Run() 200 | client, err := net.Dial("udp", "127.0.0.1:25587") 201 | if err != nil { 202 | t.Fatalf("Can't connect to the proxy: %v", err) 203 | } 204 | defer func() { _ = client.Close() }() 205 | // Make sure the proxy doesn't stop when there is no actual backend: 206 | _, _ = client.Write(testBuf) 207 | _, _ = client.Write(testBuf) 208 | backend := NewEchoServer(t, "udp", "127.0.0.1:25587") 209 | defer backend.Close() 210 | backend.Run() 211 | _ = client.SetDeadline(time.Now().Add(10 * time.Second)) 212 | if _, err = client.Write(testBuf); err != nil { 213 | t.Fatal(err) 214 | } 215 | recvBuf := make([]byte, testBufSize) 216 | if _, err = client.Read(recvBuf); err != nil { 217 | t.Fatal(err) 218 | } 219 | if !bytes.Equal(testBuf, recvBuf) { 220 | t.Fatal(fmt.Errorf("expected [%v] but got [%v]", testBuf, recvBuf)) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /nat/nat.go: -------------------------------------------------------------------------------- 1 | // Package nat is a convenience package for manipulation of strings describing network ports. 2 | package nat 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "net" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // PortBinding represents a binding between a Host IP address and a Host Port 13 | type PortBinding struct { 14 | // HostIP is the host IP Address 15 | HostIP string `json:"HostIp"` 16 | // HostPort is the host port number 17 | HostPort string 18 | } 19 | 20 | // PortMap is a collection of PortBinding indexed by Port 21 | type PortMap map[Port][]PortBinding 22 | 23 | // PortSet is a collection of structs indexed by Port 24 | type PortSet map[Port]struct{} 25 | 26 | // Port is a string containing port number and protocol in the format "80/tcp" 27 | type Port string 28 | 29 | // NewPort creates a new instance of a Port given a protocol and port number or port range 30 | func NewPort(proto, portOrRange string) (Port, error) { 31 | start, end, err := parsePortRange(portOrRange) 32 | if err != nil { 33 | return "", err 34 | } 35 | if start == end { 36 | return Port(fmt.Sprintf("%d/%s", start, proto)), nil 37 | } 38 | return Port(fmt.Sprintf("%d-%d/%s", start, end, proto)), nil 39 | } 40 | 41 | // ParsePort parses the port number string and returns an int 42 | func ParsePort(rawPort string) (int, error) { 43 | if rawPort == "" { 44 | return 0, nil 45 | } 46 | port, err := parsePortNumber(rawPort) 47 | if err != nil { 48 | return 0, fmt.Errorf("invalid port '%s': %w", rawPort, err) 49 | } 50 | return port, nil 51 | } 52 | 53 | // ParsePortRangeToInt parses the port range string and returns start/end ints 54 | func ParsePortRangeToInt(rawPort string) (startPort, endPort int, _ error) { 55 | if rawPort == "" { 56 | // TODO(thaJeztah): consider making this an error; this was kept to keep existing behavior. 57 | return 0, 0, nil 58 | } 59 | return parsePortRange(rawPort) 60 | } 61 | 62 | // Proto returns the protocol of a Port 63 | func (p Port) Proto() string { 64 | _, proto, _ := strings.Cut(string(p), "/") 65 | if proto == "" { 66 | proto = "tcp" 67 | } 68 | return proto 69 | } 70 | 71 | // Port returns the port number of a Port 72 | func (p Port) Port() string { 73 | port, _, _ := strings.Cut(string(p), "/") 74 | return port 75 | } 76 | 77 | // Int returns the port number of a Port as an int. It assumes [Port] 78 | // is valid, and returns 0 otherwise. 79 | func (p Port) Int() int { 80 | // We don't need to check for an error because we're going to 81 | // assume that any error would have been found, and reported, in [NewPort] 82 | port, _ := parsePortNumber(p.Port()) 83 | return port 84 | } 85 | 86 | // Range returns the start/end port numbers of a Port range as ints 87 | func (p Port) Range() (int, int, error) { 88 | portRange := p.Port() 89 | if portRange == "" { 90 | return 0, 0, nil 91 | } 92 | return parsePortRange(portRange) 93 | } 94 | 95 | // SplitProtoPort splits a port(range) and protocol, formatted as "/[]" 96 | // "/[]". It returns an empty string for both if 97 | // no port(range) is provided. If a port(range) is provided, but no protocol, 98 | // the default ("tcp") protocol is returned. 99 | // 100 | // SplitProtoPort does not validate or normalize the returned values. 101 | func SplitProtoPort(rawPort string) (proto string, port string) { 102 | port, proto, _ = strings.Cut(rawPort, "/") 103 | if port == "" { 104 | return "", "" 105 | } 106 | if proto == "" { 107 | proto = "tcp" 108 | } 109 | return proto, port 110 | } 111 | 112 | func validateProto(proto string) error { 113 | switch proto { 114 | case "tcp", "udp", "sctp": 115 | // All good 116 | return nil 117 | default: 118 | return errors.New("invalid proto: " + proto) 119 | } 120 | } 121 | 122 | // ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses 123 | // these in to the internal types 124 | func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) { 125 | var ( 126 | exposedPorts = make(map[Port]struct{}, len(ports)) 127 | bindings = make(map[Port][]PortBinding) 128 | ) 129 | for _, p := range ports { 130 | portMappings, err := ParsePortSpec(p) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | 135 | for _, pm := range portMappings { 136 | port := pm.Port 137 | if _, ok := exposedPorts[port]; !ok { 138 | exposedPorts[port] = struct{}{} 139 | } 140 | bindings[port] = append(bindings[port], pm.Binding) 141 | } 142 | } 143 | return exposedPorts, bindings, nil 144 | } 145 | 146 | // PortMapping is a data object mapping a Port to a PortBinding 147 | type PortMapping struct { 148 | Port Port 149 | Binding PortBinding 150 | } 151 | 152 | func (p *PortMapping) String() string { 153 | return net.JoinHostPort(p.Binding.HostIP, p.Binding.HostPort+":"+string(p.Port)) 154 | } 155 | 156 | func splitParts(rawport string) (hostIP, hostPort, containerPort string) { 157 | parts := strings.Split(rawport, ":") 158 | 159 | switch len(parts) { 160 | case 1: 161 | return "", "", parts[0] 162 | case 2: 163 | return "", parts[0], parts[1] 164 | case 3: 165 | return parts[0], parts[1], parts[2] 166 | default: 167 | n := len(parts) 168 | return strings.Join(parts[:n-2], ":"), parts[n-2], parts[n-1] 169 | } 170 | } 171 | 172 | // ParsePortSpec parses a port specification string into a slice of PortMappings 173 | func ParsePortSpec(rawPort string) ([]PortMapping, error) { 174 | ip, hostPort, containerPort := splitParts(rawPort) 175 | proto, containerPort := SplitProtoPort(containerPort) 176 | if containerPort == "" { 177 | return nil, fmt.Errorf("no port specified: %s", rawPort) 178 | } 179 | 180 | proto = strings.ToLower(proto) 181 | if err := validateProto(proto); err != nil { 182 | return nil, err 183 | } 184 | 185 | if ip != "" && ip[0] == '[' { 186 | // Strip [] from IPV6 addresses 187 | rawIP, _, err := net.SplitHostPort(ip + ":") 188 | if err != nil { 189 | return nil, fmt.Errorf("invalid IP address %v: %w", ip, err) 190 | } 191 | ip = rawIP 192 | } 193 | if ip != "" && net.ParseIP(ip) == nil { 194 | return nil, errors.New("invalid IP address: " + ip) 195 | } 196 | 197 | startPort, endPort, err := parsePortRange(containerPort) 198 | if err != nil { 199 | return nil, errors.New("invalid containerPort: " + containerPort) 200 | } 201 | 202 | var startHostPort, endHostPort int 203 | if hostPort != "" { 204 | startHostPort, endHostPort, err = parsePortRange(hostPort) 205 | if err != nil { 206 | return nil, errors.New("invalid hostPort: " + hostPort) 207 | } 208 | if (endPort - startPort) != (endHostPort - startHostPort) { 209 | // Allow host port range iff containerPort is not a range. 210 | // In this case, use the host port range as the dynamic 211 | // host port range to allocate into. 212 | if endPort != startPort { 213 | return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort) 214 | } 215 | } 216 | } 217 | 218 | count := endPort - startPort + 1 219 | ports := make([]PortMapping, 0, count) 220 | 221 | for i := 0; i < count; i++ { 222 | hPort := "" 223 | if hostPort != "" { 224 | hPort = strconv.Itoa(startHostPort + i) 225 | // Set hostPort to a range only if there is a single container port 226 | // and a dynamic host port. 227 | if count == 1 && startHostPort != endHostPort { 228 | hPort += "-" + strconv.Itoa(endHostPort) 229 | } 230 | } 231 | ports = append(ports, PortMapping{ 232 | Port: Port(strconv.Itoa(startPort+i) + "/" + proto), 233 | Binding: PortBinding{HostIP: ip, HostPort: hPort}, 234 | }) 235 | } 236 | return ports, nil 237 | } 238 | -------------------------------------------------------------------------------- /tlsconfig/config.go: -------------------------------------------------------------------------------- 1 | // Package tlsconfig provides primitives to retrieve secure-enough TLS configurations for both clients and servers. 2 | // 3 | // As a reminder from https://golang.org/pkg/crypto/tls/#Config: 4 | // 5 | // A Config structure is used to configure a TLS client or server. After one has been passed to a TLS function it must not be modified. 6 | // A Config may be reused; the tls package will also not modify it. 7 | package tlsconfig 8 | 9 | import ( 10 | "crypto/tls" 11 | "crypto/x509" 12 | "encoding/pem" 13 | "errors" 14 | "fmt" 15 | "os" 16 | ) 17 | 18 | // Options represents the information needed to create client and server TLS configurations. 19 | type Options struct { 20 | CAFile string 21 | 22 | // If either CertFile or KeyFile is empty, Client() will not load them 23 | // preventing the client from authenticating to the server. 24 | // However, Server() requires them and will error out if they are empty. 25 | CertFile string 26 | KeyFile string 27 | 28 | // client-only option 29 | InsecureSkipVerify bool 30 | // server-only option 31 | ClientAuth tls.ClientAuthType 32 | // If ExclusiveRootPools is set, then if a CA file is provided, the root pool used for TLS 33 | // creds will include exclusively the roots in that CA file. If no CA file is provided, 34 | // the system pool will be used. 35 | ExclusiveRootPools bool 36 | MinVersion uint16 37 | } 38 | 39 | // DefaultServerAcceptedCiphers should be uses by code which already has a crypto/tls 40 | // options struct but wants to use a commonly accepted set of TLS cipher suites, with 41 | // known weak algorithms removed. 42 | var DefaultServerAcceptedCiphers = defaultCipherSuites 43 | 44 | // defaultCipherSuites is shared by both client and server as the default set. 45 | var defaultCipherSuites = []uint16{ 46 | tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, 47 | tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, 48 | tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, 49 | tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, 50 | } 51 | 52 | // ServerDefault returns a secure-enough TLS configuration for the server TLS configuration. 53 | func ServerDefault(ops ...func(*tls.Config)) *tls.Config { 54 | return defaultConfig(ops...) 55 | } 56 | 57 | // ClientDefault returns a secure-enough TLS configuration for the client TLS configuration. 58 | func ClientDefault(ops ...func(*tls.Config)) *tls.Config { 59 | return defaultConfig(ops...) 60 | } 61 | 62 | // defaultConfig is the default config used by both client and server TLS configuration. 63 | func defaultConfig(ops ...func(*tls.Config)) *tls.Config { 64 | tlsConfig := &tls.Config{ 65 | // Avoid fallback by default to SSL protocols < TLS1.2 66 | MinVersion: tls.VersionTLS12, 67 | CipherSuites: defaultCipherSuites, 68 | } 69 | 70 | for _, op := range ops { 71 | op(tlsConfig) 72 | } 73 | 74 | return tlsConfig 75 | } 76 | 77 | // certPool returns an X.509 certificate pool from `caFile`, the certificate file. 78 | func certPool(caFile string, exclusivePool bool) (*x509.CertPool, error) { 79 | // If we should verify the server, we need to load a trusted ca 80 | var ( 81 | pool *x509.CertPool 82 | err error 83 | ) 84 | if exclusivePool { 85 | pool = x509.NewCertPool() 86 | } else { 87 | pool, err = SystemCertPool() 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to read system certificates: %v", err) 90 | } 91 | } 92 | pemData, err := os.ReadFile(caFile) 93 | if err != nil { 94 | return nil, fmt.Errorf("could not read CA certificate %q: %v", caFile, err) 95 | } 96 | if !pool.AppendCertsFromPEM(pemData) { 97 | return nil, fmt.Errorf("failed to append certificates from PEM file: %q", caFile) 98 | } 99 | return pool, nil 100 | } 101 | 102 | // allTLSVersions lists all the TLS versions and is used by the code that validates 103 | // a uint16 value as a TLS version. 104 | var allTLSVersions = map[uint16]struct{}{ 105 | tls.VersionTLS10: {}, 106 | tls.VersionTLS11: {}, 107 | tls.VersionTLS12: {}, 108 | tls.VersionTLS13: {}, 109 | } 110 | 111 | // isValidMinVersion checks that the input value is a valid tls minimum version 112 | func isValidMinVersion(version uint16) bool { 113 | _, ok := allTLSVersions[version] 114 | return ok 115 | } 116 | 117 | // adjustMinVersion sets the MinVersion on `config`, the input configuration. 118 | // It assumes the current MinVersion on the `config` is the lowest allowed. 119 | func adjustMinVersion(options Options, config *tls.Config) error { 120 | if options.MinVersion > 0 { 121 | if !isValidMinVersion(options.MinVersion) { 122 | return fmt.Errorf("invalid minimum TLS version: %x", options.MinVersion) 123 | } 124 | if options.MinVersion < config.MinVersion { 125 | return fmt.Errorf("requested minimum TLS version is too low. Should be at-least: %x", config.MinVersion) 126 | } 127 | config.MinVersion = options.MinVersion 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // errEncryptedKeyDeprecated is produced when we encounter an encrypted 134 | // (password-protected) key. From https://go-review.googlesource.com/c/go/+/264159; 135 | // 136 | // > Legacy PEM encryption as specified in RFC 1423 is insecure by design. Since 137 | // > it does not authenticate the ciphertext, it is vulnerable to padding oracle 138 | // > attacks that can let an attacker recover the plaintext 139 | // > 140 | // > It's unfortunate that we don't implement PKCS#8 encryption so we can't 141 | // > recommend an alternative but PEM encryption is so broken that it's worth 142 | // > deprecating outright. 143 | // 144 | // Also see https://docs.docker.com/go/deprecated/ 145 | var errEncryptedKeyDeprecated = errors.New("private key is encrypted; encrypted private keys are obsolete, and not supported") 146 | 147 | // getPrivateKey returns the private key in 'keyBytes', in PEM-encoded format. 148 | // It returns an error if the file could not be decoded or was protected by 149 | // a passphrase. 150 | func getPrivateKey(keyBytes []byte) ([]byte, error) { 151 | // this section makes some small changes to code from notary/tuf/utils/x509.go 152 | pemBlock, _ := pem.Decode(keyBytes) 153 | if pemBlock == nil { 154 | return nil, fmt.Errorf("no valid private key found") 155 | } 156 | 157 | if x509.IsEncryptedPEMBlock(pemBlock) { //nolint:staticcheck // Ignore SA1019 (IsEncryptedPEMBlock is deprecated) 158 | return nil, errEncryptedKeyDeprecated 159 | } 160 | 161 | return keyBytes, nil 162 | } 163 | 164 | // getCert returns a Certificate from the CertFile and KeyFile in 'options', 165 | // if the key is encrypted, the Passphrase in 'options' will be used to 166 | // decrypt it. 167 | func getCert(options Options) ([]tls.Certificate, error) { 168 | if options.CertFile == "" && options.KeyFile == "" { 169 | return nil, nil 170 | } 171 | 172 | cert, err := os.ReadFile(options.CertFile) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | prKeyBytes, err := os.ReadFile(options.KeyFile) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | prKeyBytes, err = getPrivateKey(prKeyBytes) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | tlsCert, err := tls.X509KeyPair(cert, prKeyBytes) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return []tls.Certificate{tlsCert}, nil 193 | } 194 | 195 | // Client returns a TLS configuration meant to be used by a client. 196 | func Client(options Options) (*tls.Config, error) { 197 | tlsConfig := defaultConfig() 198 | tlsConfig.InsecureSkipVerify = options.InsecureSkipVerify 199 | if !options.InsecureSkipVerify && options.CAFile != "" { 200 | CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) 201 | if err != nil { 202 | return nil, err 203 | } 204 | tlsConfig.RootCAs = CAs 205 | } 206 | 207 | tlsCerts, err := getCert(options) 208 | if err != nil { 209 | return nil, fmt.Errorf("could not load X509 key pair: %w", err) 210 | } 211 | tlsConfig.Certificates = tlsCerts 212 | 213 | if err := adjustMinVersion(options, tlsConfig); err != nil { 214 | return nil, err 215 | } 216 | 217 | return tlsConfig, nil 218 | } 219 | 220 | // Server returns a TLS configuration meant to be used by a server. 221 | func Server(options Options) (*tls.Config, error) { 222 | tlsConfig := defaultConfig() 223 | tlsConfig.ClientAuth = options.ClientAuth 224 | tlsCert, err := tls.LoadX509KeyPair(options.CertFile, options.KeyFile) 225 | if err != nil { 226 | if os.IsNotExist(err) { 227 | return nil, fmt.Errorf("could not load X509 key pair (cert: %q, key: %q): %v", options.CertFile, options.KeyFile, err) 228 | } 229 | return nil, fmt.Errorf("error reading X509 key pair - make sure the key is not encrypted (cert: %q, key: %q): %v", options.CertFile, options.KeyFile, err) 230 | } 231 | tlsConfig.Certificates = []tls.Certificate{tlsCert} 232 | if options.ClientAuth >= tls.VerifyClientCertIfGiven && options.CAFile != "" { 233 | CAs, err := certPool(options.CAFile, options.ExclusiveRootPools) 234 | if err != nil { 235 | return nil, err 236 | } 237 | tlsConfig.ClientCAs = CAs 238 | } 239 | 240 | if err := adjustMinVersion(options, tlsConfig); err != nil { 241 | return nil, err 242 | } 243 | 244 | return tlsConfig, nil 245 | } 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2015 Docker, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /tlsconfig/config_test.go: -------------------------------------------------------------------------------- 1 | package tlsconfig 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "crypto/x509" 7 | "encoding/pem" 8 | "errors" 9 | "os" 10 | "reflect" 11 | "runtime" 12 | "testing" 13 | ) 14 | 15 | // This is the currently active Amazon Root CA 1 (CN=Amazon Root CA 1,O=Amazon,C=US), 16 | // downloaded from: https://www.amazontrust.com/repository/AmazonRootCA1.pem 17 | // It's valid since May 26 00:00:00 2015 GMT and expires on Jan 17 00:00:00 2038 GMT. 18 | // Download updated versions from https://www.amazontrust.com/repository/ 19 | const ( 20 | systemRootTrustedCert = ` 21 | -----BEGIN CERTIFICATE----- 22 | MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF 23 | ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 24 | b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL 25 | MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv 26 | b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj 27 | ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM 28 | 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw 29 | IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 30 | VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L 31 | 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm 32 | jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC 33 | AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA 34 | A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI 35 | U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs 36 | N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv 37 | o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 38 | 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy 39 | rqXRfboQnoZsG4q5WTP468SQvvG5 40 | -----END CERTIFICATE----- 41 | ` 42 | rsaPrivateKeyFile = "fixtures/key.pem" 43 | certificateFile = "fixtures/cert.pem" 44 | multiCertificateFile = "fixtures/multi.pem" 45 | rsaEncryptedPrivateKeyFile = "fixtures/encrypted_key.pem" // TODO add code to regenerate in fixtures/generate.go 46 | certificateOfEncryptedKeyFile = "fixtures/cert_of_encrypted_key.pem" // TODO add code to regenerate in fixtures/generate.go 47 | ) 48 | 49 | // returns the name of a pre-generated, multiple-certificate CA file 50 | // with both RSA and ECDSA certs. 51 | func getMultiCert() string { 52 | return multiCertificateFile 53 | } 54 | 55 | // returns the names of pre-generated key and certificate files. 56 | func getCertAndKey() (string, string) { 57 | return rsaPrivateKeyFile, certificateFile 58 | } 59 | 60 | // returns the names of pre-generated, encrypted private key and 61 | // corresponding certificate file 62 | func getCertAndEncryptedKey() (string, string) { 63 | return rsaEncryptedPrivateKeyFile, certificateOfEncryptedKeyFile 64 | } 65 | 66 | // If the cert files and directory are provided but are invalid, an error is 67 | // returned. 68 | func TestConfigServerTLSFailsIfUnableToLoadCerts(t *testing.T) { 69 | key, cert := getCertAndKey() 70 | ca := getMultiCert() 71 | 72 | tempFile, err := os.CreateTemp("", "cert-test") 73 | if err != nil { 74 | t.Fatal("Unable to create temporary empty file") 75 | } 76 | defer func() { _ = os.RemoveAll(tempFile.Name()) }() 77 | _ = tempFile.Close() 78 | 79 | for _, badFile := range []string{"not-a-file", tempFile.Name()} { 80 | for i := 0; i < 3; i++ { 81 | files := []string{cert, key, ca} 82 | files[i] = badFile 83 | 84 | result, err := Server(Options{ 85 | CertFile: files[0], 86 | KeyFile: files[1], 87 | CAFile: files[2], 88 | ClientAuth: tls.VerifyClientCertIfGiven, 89 | }) 90 | if err == nil || result != nil { 91 | t.Fatal("Expected a non-real file to error and return a nil TLS config") 92 | } 93 | } 94 | } 95 | } 96 | 97 | // If server cert and key are provided and client auth and client CA are not 98 | // set, a tls config with only the server certs will be returned. 99 | func TestConfigServerTLSServerCertsOnly(t *testing.T) { 100 | key, cert := getCertAndKey() 101 | 102 | keypair, err := tls.LoadX509KeyPair(cert, key) 103 | if err != nil { 104 | t.Fatal("Unable to load the generated cert and key") 105 | } 106 | 107 | tlsConfig, err := Server(Options{ 108 | CertFile: cert, 109 | KeyFile: key, 110 | }) 111 | if err != nil || tlsConfig == nil { 112 | t.Fatal("Unable to configure server TLS", err) 113 | } 114 | 115 | if len(tlsConfig.Certificates) != 1 { 116 | t.Fatal("Unexpected server certificates") 117 | } 118 | if len(tlsConfig.Certificates[0].Certificate) != len(keypair.Certificate) { 119 | t.Fatal("Unexpected server certificates") 120 | } 121 | for i, cert := range tlsConfig.Certificates[0].Certificate { 122 | if !bytes.Equal(cert, keypair.Certificate[i]) { 123 | t.Fatal("Unexpected server certificates") 124 | } 125 | } 126 | 127 | if !reflect.DeepEqual(tlsConfig.CipherSuites, DefaultServerAcceptedCiphers) { 128 | t.Fatal("Unexpected server cipher suites") 129 | } 130 | if tlsConfig.MinVersion != tls.VersionTLS12 { 131 | t.Fatal("Unexpected server TLS version") 132 | } 133 | } 134 | 135 | // If client CA is provided, it will only be used if the client auth is >= 136 | // VerifyClientCertIfGiven 137 | func TestConfigServerTLSClientCANotSetIfClientAuthTooLow(t *testing.T) { 138 | key, cert := getCertAndKey() 139 | ca := getMultiCert() 140 | 141 | tlsConfig, err := Server(Options{ 142 | CertFile: cert, 143 | KeyFile: key, 144 | ClientAuth: tls.RequestClientCert, 145 | CAFile: ca, 146 | }) 147 | 148 | if err != nil || tlsConfig == nil { 149 | t.Fatal("Unable to configure server TLS", err) 150 | } 151 | 152 | if len(tlsConfig.Certificates) != 1 { 153 | t.Fatal("Unexpected server certificates") 154 | } 155 | if tlsConfig.ClientAuth != tls.RequestClientCert { 156 | t.Fatal("ClientAuth was not set to what was in the options") 157 | } 158 | if tlsConfig.ClientCAs != nil { //nolint:staticcheck // Ignore SA1019: tlsConfig.ClientCAs.Subjects has been deprecated since Go 1.18: if s was returned by SystemCertPool, Subjects will not include the system roots. 159 | t.Fatalf("Client CAs should never have been set") 160 | } 161 | } 162 | 163 | // If client CA is provided, it will only be used if the client auth is >= 164 | // VerifyClientCertIfGiven 165 | func TestConfigServerTLSClientCASet(t *testing.T) { 166 | key, cert := getCertAndKey() 167 | ca := getMultiCert() 168 | 169 | tlsConfig, err := Server(Options{ 170 | CertFile: cert, 171 | KeyFile: key, 172 | ClientAuth: tls.VerifyClientCertIfGiven, 173 | CAFile: ca, 174 | }) 175 | 176 | if err != nil || tlsConfig == nil { 177 | t.Fatal("Unable to configure server TLS", err) 178 | } 179 | 180 | if len(tlsConfig.Certificates) != 1 { 181 | t.Fatal("Unexpected server certificates") 182 | } 183 | if tlsConfig.ClientAuth != tls.VerifyClientCertIfGiven { 184 | t.Fatal("ClientAuth was not set to what was in the options") 185 | } 186 | basePool, err := SystemCertPool() 187 | if err != nil { 188 | basePool = x509.NewCertPool() 189 | } 190 | // because we are not enabling `ExclusiveRootPools`, any root pool will also contain the system roots 191 | if tlsConfig.ClientCAs == nil || len(tlsConfig.ClientCAs.Subjects()) != len(basePool.Subjects())+2 { //nolint:staticcheck // Ignore SA1019: tlsConfig.ClientCAs.Subjects has been deprecated since Go 1.18: if s was returned by SystemCertPool, Subjects will not include the system roots. 192 | t.Fatalf("Client CAs were never set correctly") 193 | } 194 | } 195 | 196 | // Exclusive root pools determines whether the CA pool will be a union of the system 197 | // certificate pool and custom certs, or an exclusive or of the custom certs and system pool 198 | func TestConfigServerExclusiveRootPools(t *testing.T) { 199 | if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { 200 | // FIXME: see https://github.com/docker/go-connections/issues/105. 201 | t.Skip("FIXME: failing on Windows and darwin") 202 | } 203 | key, cert := getCertAndKey() 204 | ca := getMultiCert() 205 | 206 | caBytes, err := os.ReadFile(ca) 207 | if err != nil { 208 | t.Fatal("Unable to read CA certs", err) 209 | } 210 | 211 | var testCerts []*x509.Certificate 212 | for _, pemBytes := range [][]byte{caBytes, []byte(systemRootTrustedCert)} { 213 | pemBlock, _ := pem.Decode(pemBytes) 214 | if pemBlock == nil { 215 | t.Fatal("Malformed certificate") 216 | } 217 | cert, err := x509.ParseCertificate(pemBlock.Bytes) 218 | if err != nil { 219 | t.Fatal("Unable to parse certificate") 220 | } 221 | testCerts = append(testCerts, cert) 222 | } 223 | 224 | // ExclusiveRootPools not set, so should be able to verify both system-signed certs 225 | // and custom CA-signed certs 226 | tlsConfig, err := Server(Options{ 227 | CertFile: cert, 228 | KeyFile: key, 229 | ClientAuth: tls.VerifyClientCertIfGiven, 230 | CAFile: ca, 231 | }) 232 | 233 | if err != nil || tlsConfig == nil { 234 | t.Fatal("Unable to configure server TLS", err) 235 | } 236 | 237 | for i, cert := range testCerts { 238 | if _, err := cert.Verify(x509.VerifyOptions{Roots: tlsConfig.ClientCAs}); err != nil { 239 | t.Fatalf("Unable to verify certificate %d: %v", i, err) 240 | } 241 | } 242 | 243 | // ExclusiveRootPools set and custom CA provided, so system certs should not be verifiable 244 | // and custom CA-signed certs should be verifiable 245 | tlsConfig, err = Server(Options{ 246 | CertFile: cert, 247 | KeyFile: key, 248 | ClientAuth: tls.VerifyClientCertIfGiven, 249 | CAFile: ca, 250 | ExclusiveRootPools: true, 251 | }) 252 | 253 | if err != nil || tlsConfig == nil { 254 | t.Fatal("Unable to configure server TLS", err) 255 | } 256 | 257 | for i, cert := range testCerts { 258 | _, err := cert.Verify(x509.VerifyOptions{Roots: tlsConfig.ClientCAs}) 259 | switch { 260 | case i == 0 && err != nil: 261 | t.Fatal("Unable to verify custom certificate, even though the root pool should have only the custom CA", err) 262 | case i == 1 && err == nil: 263 | t.Fatal("Successfully verified system root-signed certificate though the root pool should have only the cusotm CA", err) 264 | } 265 | } 266 | 267 | // No CA file provided, system cert should be verifiable only 268 | tlsConfig, err = Server(Options{ 269 | CertFile: cert, 270 | KeyFile: key, 271 | }) 272 | 273 | if err != nil || tlsConfig == nil { 274 | t.Fatal("Unable to configure server TLS", err) 275 | } 276 | 277 | for i, cert := range testCerts { 278 | _, err := cert.Verify(x509.VerifyOptions{Roots: tlsConfig.ClientCAs}) 279 | switch { 280 | case i == 1 && err != nil: 281 | t.Fatal("Unable to verify system root-signed certificate, even though the root pool should be the system pool only", err) 282 | case i == 0 && err == nil: 283 | t.Fatal("Successfully verified custom certificate though the root pool should be the system pool only", err) 284 | } 285 | } 286 | } 287 | 288 | // If we provide a modifier to the server's default TLS configuration generator, it 289 | // should be applied accordingly 290 | func TestConfigServerDefaultWithTLSMinimumModifier(t *testing.T) { 291 | tlsVersions := []uint16{ 292 | tls.VersionTLS11, 293 | tls.VersionTLS12, 294 | } 295 | 296 | for _, tlsVersion := range tlsVersions { 297 | servDefault := ServerDefault(func(c *tls.Config) { 298 | c.MinVersion = tlsVersion 299 | }) 300 | 301 | if servDefault.MinVersion != tlsVersion { 302 | t.Fatalf("Unexpected min TLS version for default server TLS config: %d", servDefault.MinVersion) 303 | } 304 | } 305 | } 306 | 307 | // If we provide a modifier to the client's default TLS configuration generator, it 308 | // should be applied accordingly 309 | func TestConfigClientDefaultWithTLSMinimumModifier(t *testing.T) { 310 | tlsVersions := []uint16{ 311 | tls.VersionTLS11, 312 | tls.VersionTLS12, 313 | } 314 | 315 | for _, tlsVersion := range tlsVersions { 316 | clientDefault := ClientDefault(func(c *tls.Config) { 317 | c.MinVersion = tlsVersion 318 | }) 319 | 320 | if clientDefault.MinVersion != tlsVersion { 321 | t.Fatalf("Unexpected min TLS version for default client TLS config: %d", clientDefault.MinVersion) 322 | } 323 | } 324 | } 325 | 326 | // If a valid minimum version is specified in the options, the server's 327 | // minimum version should be set accordingly 328 | func TestConfigServerTLSMinVersionIsSetBasedOnOptions(t *testing.T) { 329 | versions := []uint16{ 330 | tls.VersionTLS12, 331 | } 332 | key, cert := getCertAndKey() 333 | 334 | for _, v := range versions { 335 | tlsConfig, err := Server(Options{ 336 | MinVersion: v, 337 | CertFile: cert, 338 | KeyFile: key, 339 | }) 340 | 341 | if err != nil || tlsConfig == nil { 342 | t.Fatal("Unable to configure server TLS", err) 343 | } 344 | 345 | if tlsConfig.MinVersion != v { 346 | t.Fatal("Unexpected minimum TLS version: ", tlsConfig.MinVersion) 347 | } 348 | } 349 | } 350 | 351 | // An error should be returned if the specified minimum version for the server 352 | // is too low, i.e. less than VersionTLS10 353 | func TestConfigServerTLSMinVersionNotSetIfMinVersionIsTooLow(t *testing.T) { 354 | key, cert := getCertAndKey() 355 | 356 | _, err := Server(Options{ 357 | MinVersion: tls.VersionTLS10, 358 | CertFile: cert, 359 | KeyFile: key, 360 | }) 361 | 362 | if err == nil { 363 | t.Fatal("Should have returned an error for minimum version below TLS10") 364 | } 365 | } 366 | 367 | // An error should be returned if an invalid minimum version for the server is 368 | // in the options struct 369 | func TestConfigServerTLSMinVersionNotSetIfMinVersionIsInvalid(t *testing.T) { 370 | key, cert := getCertAndKey() 371 | 372 | _, err := Server(Options{ 373 | MinVersion: 1, 374 | CertFile: cert, 375 | KeyFile: key, 376 | }) 377 | 378 | if err == nil { 379 | t.Fatal("Should have returned error on invalid minimum version option") 380 | } 381 | } 382 | 383 | // The root CA is never set if InsecureSkipBoolean is set to true, but the 384 | // default client options are set 385 | func TestConfigClientTLSNoVerify(t *testing.T) { 386 | ca := getMultiCert() 387 | 388 | tlsConfig, err := Client(Options{CAFile: ca, InsecureSkipVerify: true}) 389 | 390 | if err != nil || tlsConfig == nil { 391 | t.Fatal("Unable to configure client TLS", err) 392 | } 393 | 394 | if tlsConfig.RootCAs != nil { //nolint:staticcheck // Ignore SA1019: tlsConfig.RootCAs.Subjects has been deprecated since Go 1.18: if s was returned by SystemCertPool, Subjects will not include the system roots. 395 | t.Fatal("Should not have set Root CAs", err) 396 | } 397 | 398 | if !reflect.DeepEqual(tlsConfig.CipherSuites, defaultCipherSuites) { 399 | t.Fatal("Unexpected client cipher suites") 400 | } 401 | if tlsConfig.MinVersion != tls.VersionTLS12 { 402 | t.Fatal("Unexpected client TLS version") 403 | } 404 | 405 | if tlsConfig.Certificates != nil { 406 | t.Fatal("Somehow client certificates were set") 407 | } 408 | } 409 | 410 | // The root CA is never set if InsecureSkipBoolean is set to false and root CA 411 | // is not provided. 412 | func TestConfigClientTLSNoRoot(t *testing.T) { 413 | tlsConfig, err := Client(Options{}) 414 | 415 | if err != nil || tlsConfig == nil { 416 | t.Fatal("Unable to configure client TLS", err) 417 | } 418 | 419 | if tlsConfig.RootCAs != nil { 420 | t.Fatal("Should not have set Root CAs", err) 421 | } 422 | 423 | if !reflect.DeepEqual(tlsConfig.CipherSuites, defaultCipherSuites) { 424 | t.Fatal("Unexpected client cipher suites") 425 | } 426 | if tlsConfig.MinVersion != tls.VersionTLS12 { 427 | t.Fatal("Unexpected client TLS version") 428 | } 429 | 430 | if tlsConfig.Certificates != nil { 431 | t.Fatal("Somehow client certificates were set") 432 | } 433 | } 434 | 435 | // The RootCA is set if the file is provided and InsecureSkipVerify is false 436 | func TestConfigClientTLSRootCAFileWithOneCert(t *testing.T) { 437 | ca := getMultiCert() 438 | 439 | tlsConfig, err := Client(Options{CAFile: ca}) 440 | 441 | if err != nil || tlsConfig == nil { 442 | t.Fatal("Unable to configure client TLS", err) 443 | } 444 | basePool, err := SystemCertPool() 445 | if err != nil { 446 | basePool = x509.NewCertPool() 447 | } 448 | // because we are not enabling `ExclusiveRootPools`, any root pool will also contain the system roots 449 | if tlsConfig.RootCAs == nil || len(tlsConfig.RootCAs.Subjects()) != len(basePool.Subjects())+2 { //nolint:staticcheck // Ignore SA1019: tlsConfig.ClientCAs.Subjects has been deprecated since Go 1.18: if s was returned by SystemCertPool, Subjects will not include the system roots. 450 | t.Fatal("Root CAs not set properly", err) 451 | } 452 | if tlsConfig.Certificates != nil { 453 | t.Fatal("Somehow client certificates were set") 454 | } 455 | } 456 | 457 | // An error is returned if a root CA is provided but the file doesn't exist. 458 | func TestConfigClientTLSNonexistentRootCAFile(t *testing.T) { 459 | tlsConfig, err := Client(Options{CAFile: "nonexistent"}) 460 | 461 | if err == nil || tlsConfig != nil { 462 | t.Fatal("Should not have been able to configure client TLS", err) 463 | } 464 | } 465 | 466 | // An error is returned if either the client cert or the key are provided 467 | // but invalid or blank. 468 | func TestConfigClientTLSClientCertOrKeyInvalid(t *testing.T) { 469 | key, cert := getCertAndKey() 470 | 471 | tempFile, err := os.CreateTemp("", "cert-test") 472 | if err != nil { 473 | t.Fatal("Unable to create temporary empty file") 474 | } 475 | defer func() { _ = os.Remove(tempFile.Name()) }() 476 | _ = tempFile.Close() 477 | 478 | for i := 0; i < 2; i++ { 479 | for _, invalid := range []string{"not-a-file", "", tempFile.Name()} { 480 | files := []string{cert, key} 481 | files[i] = invalid 482 | 483 | tlsConfig, err := Client(Options{CertFile: files[0], KeyFile: files[1]}) 484 | if err == nil || tlsConfig != nil { 485 | t.Fatal("Should not have been able to configure client TLS", err) 486 | } 487 | } 488 | } 489 | } 490 | 491 | // The certificate is set if the client cert and client key are provided and 492 | // valid. 493 | func TestConfigClientTLSValidClientCertAndKey(t *testing.T) { 494 | key, cert := getCertAndKey() 495 | 496 | keypair, err := tls.LoadX509KeyPair(cert, key) 497 | if err != nil { 498 | t.Fatal("Unable to load the generated cert and key") 499 | } 500 | 501 | tlsConfig, err := Client(Options{CertFile: cert, KeyFile: key}) 502 | 503 | if err != nil || tlsConfig == nil { 504 | t.Fatal("Unable to configure client TLS", err) 505 | } 506 | 507 | if len(tlsConfig.Certificates) != 1 { 508 | t.Fatal("Unexpected client certificates") 509 | } 510 | if len(tlsConfig.Certificates[0].Certificate) != len(keypair.Certificate) { 511 | t.Fatal("Unexpected client certificates") 512 | } 513 | for i, cert := range tlsConfig.Certificates[0].Certificate { 514 | if !bytes.Equal(cert, keypair.Certificate[i]) { 515 | t.Fatal("Unexpected client certificates") 516 | } 517 | } 518 | 519 | if tlsConfig.RootCAs != nil { 520 | t.Fatal("Root CAs should not have been set", err) 521 | } 522 | } 523 | 524 | func TestConfigClientTLSEncryptedKey(t *testing.T) { 525 | key, cert := getCertAndEncryptedKey() 526 | 527 | tlsConfig, err := Client(Options{ 528 | CertFile: cert, 529 | KeyFile: key, 530 | }) 531 | if !errors.Is(err, errEncryptedKeyDeprecated) { 532 | t.Errorf("Expected %v but got %v", errEncryptedKeyDeprecated, err) 533 | } 534 | if tlsConfig != nil { 535 | t.Errorf("Expected nil but got %v", tlsConfig) 536 | } 537 | } 538 | 539 | // Exclusive root pools determines whether the CA pool will be a union of the system 540 | // certificate pool and custom certs, or an exclusive or of the custom certs and system pool 541 | func TestConfigClientExclusiveRootPools(t *testing.T) { 542 | if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { 543 | // FIXME: see https://github.com/docker/go-connections/issues/105. 544 | t.Skip("FIXME: failing on Windows and darwin") 545 | } 546 | ca := getMultiCert() 547 | 548 | caBytes, err := os.ReadFile(ca) 549 | if err != nil { 550 | t.Fatal("Unable to read CA certs", err) 551 | } 552 | 553 | var testCerts []*x509.Certificate 554 | for _, pemBytes := range [][]byte{caBytes, []byte(systemRootTrustedCert)} { 555 | pemBlock, _ := pem.Decode(pemBytes) 556 | if pemBlock == nil { 557 | t.Fatal("Malformed certificate") 558 | } 559 | cert, err := x509.ParseCertificate(pemBlock.Bytes) 560 | if err != nil { 561 | t.Fatal("Unable to parse certificate") 562 | } 563 | testCerts = append(testCerts, cert) 564 | } 565 | 566 | // ExclusiveRootPools not set, so should be able to verify both system-signed certs 567 | // and custom CA-signed certs 568 | tlsConfig, err := Client(Options{CAFile: ca}) 569 | 570 | if err != nil || tlsConfig == nil { 571 | t.Fatal("Unable to configure client TLS", err) 572 | } 573 | 574 | for i, cert := range testCerts { 575 | if _, err := cert.Verify(x509.VerifyOptions{Roots: tlsConfig.RootCAs}); err != nil { 576 | t.Fatalf("Unable to verify certificate %d: %v", i, err) 577 | } 578 | } 579 | 580 | // ExclusiveRootPools set and custom CA provided, so system certs should not be verifiable 581 | // and custom CA-signed certs should be verifiable 582 | tlsConfig, err = Client(Options{ 583 | CAFile: ca, 584 | ExclusiveRootPools: true, 585 | }) 586 | 587 | if err != nil || tlsConfig == nil { 588 | t.Fatal("Unable to configure client TLS", err) 589 | } 590 | 591 | for i, cert := range testCerts { 592 | _, err := cert.Verify(x509.VerifyOptions{Roots: tlsConfig.RootCAs}) 593 | switch { 594 | case i == 0 && err != nil: 595 | t.Fatal("Unable to verify custom certificate, even though the root pool should have only the custom CA", err) 596 | case i == 1 && err == nil: 597 | t.Fatal("Successfully verified system root-signed certificate though the root pool should have only the cusotm CA", err) 598 | } 599 | } 600 | 601 | // No CA file provided, system cert should be verifiable only 602 | tlsConfig, err = Client(Options{}) 603 | 604 | if err != nil || tlsConfig == nil { 605 | t.Fatal("Unable to configure client TLS", err) 606 | } 607 | 608 | for i, cert := range testCerts { 609 | _, err := cert.Verify(x509.VerifyOptions{Roots: tlsConfig.RootCAs}) 610 | switch { 611 | case i == 1 && err != nil: 612 | t.Fatal("Unable to verify system root-signed certificate, even though the root pool should be the system pool only", err) 613 | case i == 0 && err == nil: 614 | t.Fatal("Successfully verified custom certificate though the root pool should be the system pool only", err) 615 | } 616 | } 617 | } 618 | 619 | // If a valid MinVersion is specified in the options, the client's 620 | // minimum version should be set accordingly 621 | func TestConfigClientTLSMinVersionIsSetBasedOnOptions(t *testing.T) { 622 | key, cert := getCertAndKey() 623 | 624 | tlsConfig, err := Client(Options{ 625 | MinVersion: tls.VersionTLS12, 626 | CertFile: cert, 627 | KeyFile: key, 628 | }) 629 | 630 | if err != nil || tlsConfig == nil { 631 | t.Fatal("Unable to configure client TLS", err) 632 | } 633 | 634 | if tlsConfig.MinVersion != tls.VersionTLS12 { 635 | t.Fatal("Unexpected minimum TLS version: ", tlsConfig.MinVersion) 636 | } 637 | } 638 | 639 | // An error should be returned if the specified minimum version for the client 640 | // is too low, i.e. less than VersionTLS12 641 | func TestConfigClientTLSMinVersionNotSetIfMinVersionIsTooLow(t *testing.T) { 642 | key, cert := getCertAndKey() 643 | 644 | _, err := Client(Options{ 645 | MinVersion: tls.VersionTLS11, 646 | CertFile: cert, 647 | KeyFile: key, 648 | }) 649 | 650 | if err == nil { 651 | t.Fatal("Should have returned an error for minimum version below TLS12") 652 | } 653 | } 654 | 655 | // An error should be returned if an invalid minimum version for the client is 656 | // in the options struct 657 | func TestConfigClientTLSMinVersionNotSetIfMinVersionIsInvalid(t *testing.T) { 658 | key, cert := getCertAndKey() 659 | 660 | _, err := Client(Options{ 661 | MinVersion: 1, 662 | CertFile: cert, 663 | KeyFile: key, 664 | }) 665 | 666 | if err == nil { 667 | t.Fatal("Should have returned error on invalid minimum version option") 668 | } 669 | } 670 | -------------------------------------------------------------------------------- /nat/nat_test.go: -------------------------------------------------------------------------------- 1 | package nat 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestParsePort(t *testing.T) { 9 | tests := []struct { 10 | doc string 11 | input string 12 | expPort int 13 | expErr string 14 | }{ 15 | { 16 | doc: "invalid value", 17 | input: "asdf", 18 | expPort: 0, 19 | expErr: `invalid port 'asdf': invalid syntax`, 20 | }, 21 | { 22 | doc: "invalid value with number", 23 | input: "1asdf", 24 | expPort: 0, 25 | expErr: `invalid port '1asdf': invalid syntax`, 26 | }, 27 | { 28 | doc: "empty value", 29 | input: "", 30 | expPort: 0, 31 | }, 32 | { 33 | doc: "zero value", 34 | input: "0", 35 | expPort: 0, 36 | }, 37 | { 38 | doc: "negative value", 39 | input: "-1", 40 | expPort: 0, 41 | expErr: `invalid port '-1': value out of range (0–65535)`, 42 | }, 43 | // FIXME currently this is a valid port. I don't think it should be. 44 | // I'm leaving this test until we make a decision. 45 | // - erikh 46 | { 47 | doc: "octal value", 48 | input: "0123", 49 | expPort: 123, 50 | }, 51 | { 52 | doc: "max value", 53 | input: "65535", 54 | expPort: 65535, 55 | }, 56 | { 57 | doc: "value out of range", 58 | input: "65536", 59 | expPort: 0, 60 | expErr: `invalid port '65536': value out of range (0–65535)`, 61 | }, 62 | } 63 | 64 | for _, tc := range tests { 65 | t.Run(tc.doc, func(t *testing.T) { 66 | port, err := ParsePort(tc.input) 67 | if tc.expErr != "" { 68 | if err == nil || err.Error() != tc.expErr { 69 | t.Errorf("expected error '%s', got '%v'", tc.expErr, err.Error()) 70 | } 71 | } else { 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | } 76 | if port != tc.expPort { 77 | t.Errorf("expected port %d, got %d", tc.expPort, port) 78 | } 79 | 80 | }) 81 | } 82 | } 83 | 84 | // TestParsePortRangeToInt tests behavior that's specific to [ParsePortRangeToInt], 85 | // which is a shallow wrapper around [ParsePortRange], except for returning int's, 86 | // and accepting empty values. Other cases are covered by [TestParsePortRange]. 87 | func TestParsePortRangeToInt(t *testing.T) { 88 | _, _, err := ParsePortRangeToInt("") 89 | if err != nil { 90 | t.Error(err) 91 | } 92 | begin, end, err := ParsePortRangeToInt("8000-9000") 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | if expBegin := 8000; begin != 8000 { 97 | t.Errorf("expected begin %d, got %d", expBegin, begin) 98 | } 99 | if expEnd := 9000; end != expEnd { 100 | t.Errorf("expected end %d, got %d", expEnd, end) 101 | } 102 | } 103 | 104 | func TestPort(t *testing.T) { 105 | p, err := NewPort("tcp", "1234") 106 | if err != nil { 107 | t.Fatalf("tcp, 1234 had a parsing issue: %v", err) 108 | } 109 | 110 | if string(p) != "1234/tcp" { 111 | t.Fatal("tcp, 1234 did not result in the string 1234/tcp") 112 | } 113 | 114 | if p.Proto() != "tcp" { 115 | t.Fatal("protocol was not tcp") 116 | } 117 | 118 | if p.Port() != "1234" { 119 | t.Fatal("port string value was not 1234") 120 | } 121 | 122 | if p.Int() != 1234 { 123 | t.Fatal("port int value was not 1234") 124 | } 125 | 126 | _, err = NewPort("tcp", "asd1234") 127 | if err == nil { 128 | t.Fatal("tcp, asd1234 was supposed to fail") 129 | } 130 | 131 | _, err = NewPort("tcp", "1234-1230") 132 | if err == nil { 133 | t.Fatal("tcp, 1234-1230 was supposed to fail") 134 | } 135 | 136 | p, err = NewPort("tcp", "1234-1242") 137 | if err != nil { 138 | t.Fatalf("tcp, 1234-1242 had a parsing issue: %v", err) 139 | } 140 | 141 | if string(p) != "1234-1242/tcp" { 142 | t.Fatal("tcp, 1234-1242 did not result in the string 1234-1242/tcp") 143 | } 144 | } 145 | 146 | func TestSplitProtoPort(t *testing.T) { 147 | tests := []struct { 148 | doc string 149 | input string 150 | expPort string 151 | expProto string 152 | }{ 153 | { 154 | doc: "empty value", 155 | }, 156 | { 157 | doc: "zero value", 158 | input: "0", 159 | expPort: "0", 160 | expProto: "tcp", 161 | }, 162 | { 163 | doc: "empty port", 164 | input: "/udp", 165 | expPort: "", 166 | expProto: "", 167 | }, 168 | { 169 | doc: "single port", 170 | input: "1234", 171 | expPort: "1234", 172 | expProto: "tcp", 173 | }, 174 | { 175 | doc: "single port with empty protocol", 176 | input: "1234/", 177 | expPort: "1234", 178 | expProto: "tcp", 179 | }, 180 | { 181 | doc: "single port with protocol", 182 | input: "1234/udp", 183 | expPort: "1234", 184 | expProto: "udp", 185 | }, 186 | { 187 | doc: "port range", 188 | input: "80-8080", 189 | expPort: "80-8080", 190 | expProto: "tcp", 191 | }, 192 | { 193 | doc: "port range with empty protocol", 194 | input: "80-8080/", 195 | expPort: "80-8080", 196 | expProto: "tcp", 197 | }, 198 | { 199 | doc: "port range with protocol", 200 | input: "80-8080/udp", 201 | expPort: "80-8080", 202 | expProto: "udp", 203 | }, 204 | 205 | // SplitProtoPort currently does not validate or normalize, so these are expected returns 206 | { 207 | doc: "negative value", 208 | input: "-1", 209 | expPort: "-1", 210 | expProto: "tcp", 211 | }, 212 | { 213 | doc: "uppercase protocol", 214 | input: "1234/UDP", 215 | expPort: "1234", 216 | expProto: "UDP", 217 | }, 218 | { 219 | doc: "any value", 220 | input: "any port value", 221 | expPort: "any port value", 222 | expProto: "tcp", 223 | }, 224 | { 225 | doc: "any value with protocol", 226 | input: "any port value/any proto value", 227 | expPort: "any port value", 228 | expProto: "any proto value", 229 | }, 230 | } 231 | for _, tc := range tests { 232 | t.Run(tc.doc, func(t *testing.T) { 233 | proto, port := SplitProtoPort(tc.input) 234 | if proto != tc.expProto { 235 | t.Errorf("expected proto %s, got %s", tc.expProto, proto) 236 | } 237 | if port != tc.expPort { 238 | t.Errorf("expected port %s, got %s", tc.expPort, port) 239 | } 240 | }) 241 | } 242 | } 243 | 244 | func TestParsePortSpecEmptyContainerPort(t *testing.T) { 245 | tests := []struct { 246 | name string 247 | spec string 248 | expError string 249 | }{ 250 | { 251 | name: "empty spec", 252 | spec: "", 253 | expError: `no port specified: `, 254 | }, 255 | { 256 | name: "empty container port", 257 | spec: `0.0.0.0:1234-1235:/tcp`, 258 | expError: `no port specified: 0.0.0.0:1234-1235:/tcp`, 259 | }, 260 | { 261 | name: "empty container port and proto", 262 | spec: `0.0.0.0:1234-1235:`, 263 | expError: `no port specified: 0.0.0.0:1234-1235:`, 264 | }, 265 | } 266 | for _, tc := range tests { 267 | t.Run(tc.name, func(t *testing.T) { 268 | _, err := ParsePortSpec(tc.spec) 269 | if err == nil || err.Error() != tc.expError { 270 | t.Fatalf("expected %v, got: %v", tc.expError, err) 271 | } 272 | }) 273 | } 274 | } 275 | 276 | func TestParsePortSpecFull(t *testing.T) { 277 | portMappings, err := ParsePortSpec("0.0.0.0:1234-1235:3333-3334/tcp") 278 | if err != nil { 279 | t.Fatalf("expected nil error, got: %v", err) 280 | } 281 | 282 | expected := []PortMapping{ 283 | { 284 | Port: "3333/tcp", 285 | Binding: PortBinding{ 286 | HostIP: "0.0.0.0", 287 | HostPort: "1234", 288 | }, 289 | }, 290 | { 291 | Port: "3334/tcp", 292 | Binding: PortBinding{ 293 | HostIP: "0.0.0.0", 294 | HostPort: "1235", 295 | }, 296 | }, 297 | } 298 | 299 | if !reflect.DeepEqual(expected, portMappings) { 300 | t.Fatalf("wrong port mappings: got=%v, want=%v", portMappings, expected) 301 | } 302 | } 303 | 304 | func TestPartPortSpecIPV6(t *testing.T) { 305 | type test struct { 306 | name string 307 | spec string 308 | expected []PortMapping 309 | } 310 | cases := []test{ 311 | { 312 | name: "square angled IPV6 without host port", 313 | spec: "[2001:4860:0:2001::68]::333", 314 | expected: []PortMapping{ 315 | { 316 | Port: "333/tcp", 317 | Binding: PortBinding{ 318 | HostIP: "2001:4860:0:2001::68", 319 | HostPort: "", 320 | }, 321 | }, 322 | }, 323 | }, 324 | { 325 | name: "square angled IPV6 with host port", 326 | spec: "[::1]:80:80", 327 | expected: []PortMapping{ 328 | { 329 | Port: "80/tcp", 330 | Binding: PortBinding{ 331 | HostIP: "::1", 332 | HostPort: "80", 333 | }, 334 | }, 335 | }, 336 | }, 337 | { 338 | name: "IPV6 without host port", 339 | spec: "2001:4860:0:2001::68::333", 340 | expected: []PortMapping{ 341 | { 342 | Port: "333/tcp", 343 | Binding: PortBinding{ 344 | HostIP: "2001:4860:0:2001::68", 345 | HostPort: "", 346 | }, 347 | }, 348 | }, 349 | }, 350 | { 351 | name: "IPV6 with host port", 352 | spec: "::1:80:80", 353 | expected: []PortMapping{ 354 | { 355 | Port: "80/tcp", 356 | Binding: PortBinding{ 357 | HostIP: "::1", 358 | HostPort: "80", 359 | }, 360 | }, 361 | }, 362 | }, 363 | { 364 | name: ":: IPV6, without host port", 365 | spec: "::::80", 366 | expected: []PortMapping{ 367 | { 368 | Port: "80/tcp", 369 | Binding: PortBinding{ 370 | HostIP: "::", 371 | HostPort: "", 372 | }, 373 | }, 374 | }, 375 | }, 376 | } 377 | for _, c := range cases { 378 | t.Run(c.name, func(t *testing.T) { 379 | portMappings, err := ParsePortSpec(c.spec) 380 | if err != nil { 381 | t.Fatalf("expected nil error, got: %v", err) 382 | } 383 | if !reflect.DeepEqual(c.expected, portMappings) { 384 | t.Fatalf("wrong port mappings: got=%v, want=%v", portMappings, c.expected) 385 | } 386 | }) 387 | } 388 | } 389 | 390 | func TestParsePortSpecs(t *testing.T) { 391 | var ( 392 | portMap map[Port]struct{} 393 | bindingMap map[Port][]PortBinding 394 | err error 395 | ) 396 | 397 | portMap, bindingMap, err = ParsePortSpecs([]string{"1234/tcp", "2345/udp", "3456/sctp"}) 398 | if err != nil { 399 | t.Fatalf("Error while processing ParsePortSpecs: %s", err) 400 | } 401 | 402 | if _, ok := portMap["1234/tcp"]; !ok { 403 | t.Fatal("1234/tcp was not parsed properly") 404 | } 405 | 406 | if _, ok := portMap["2345/udp"]; !ok { 407 | t.Fatal("2345/udp was not parsed properly") 408 | } 409 | 410 | if _, ok := portMap["3456/sctp"]; !ok { 411 | t.Fatal("3456/sctp was not parsed properly") 412 | } 413 | 414 | for portSpec, bindings := range bindingMap { 415 | if len(bindings) != 1 { 416 | t.Fatalf("%s should have exactly one binding", portSpec) 417 | } 418 | 419 | if bindings[0].HostIP != "" { 420 | t.Fatalf("HostIP should not be set for %s", portSpec) 421 | } 422 | 423 | if bindings[0].HostPort != "" { 424 | t.Fatalf("HostPort should not be set for %s", portSpec) 425 | } 426 | } 427 | 428 | portMap, bindingMap, err = ParsePortSpecs([]string{"1234:1234/tcp", "2345:2345/udp", "3456:3456/sctp"}) 429 | if err != nil { 430 | t.Fatalf("Error while processing ParsePortSpecs: %s", err) 431 | } 432 | 433 | if _, ok := portMap["1234/tcp"]; !ok { 434 | t.Fatal("1234/tcp was not parsed properly") 435 | } 436 | 437 | if _, ok := portMap["2345/udp"]; !ok { 438 | t.Fatal("2345/udp was not parsed properly") 439 | } 440 | 441 | if _, ok := portMap["3456/sctp"]; !ok { 442 | t.Fatal("3456/sctp was not parsed properly") 443 | } 444 | 445 | for portSpec, bindings := range bindingMap { 446 | _, port := SplitProtoPort(string(portSpec)) 447 | 448 | if len(bindings) != 1 { 449 | t.Fatalf("%s should have exactly one binding", portSpec) 450 | } 451 | 452 | if bindings[0].HostIP != "" { 453 | t.Fatalf("HostIP should not be set for %s", portSpec) 454 | } 455 | 456 | if bindings[0].HostPort != port { 457 | t.Fatalf("HostPort should be %s for %s", port, portSpec) 458 | } 459 | } 460 | 461 | portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp", "0.0.0.0:3456:3456/sctp"}) 462 | if err != nil { 463 | t.Fatalf("Error while processing ParsePortSpecs: %s", err) 464 | } 465 | 466 | if _, ok := portMap["1234/tcp"]; !ok { 467 | t.Fatal("1234/tcp was not parsed properly") 468 | } 469 | 470 | if _, ok := portMap["2345/udp"]; !ok { 471 | t.Fatal("2345/udp was not parsed properly") 472 | } 473 | 474 | if _, ok := portMap["3456/sctp"]; !ok { 475 | t.Fatal("3456/sctp was not parsed properly") 476 | } 477 | 478 | for portSpec, bindings := range bindingMap { 479 | _, port := SplitProtoPort(string(portSpec)) 480 | 481 | if len(bindings) != 1 { 482 | t.Fatalf("%s should have exactly one binding", portSpec) 483 | } 484 | 485 | if bindings[0].HostIP != "0.0.0.0" { 486 | t.Fatalf("HostIP is not 0.0.0.0 for %s", portSpec) 487 | } 488 | 489 | if bindings[0].HostPort != port { 490 | t.Fatalf("HostPort should be %s for %s", port, portSpec) 491 | } 492 | } 493 | 494 | _, _, err = ParsePortSpecs([]string{"localhost:1234:1234/tcp"}) 495 | 496 | if err == nil { 497 | t.Fatal("Received no error while trying to parse a hostname instead of ip") 498 | } 499 | } 500 | 501 | func TestParsePortSpecsWithRange(t *testing.T) { 502 | var ( 503 | portMap map[Port]struct{} 504 | bindingMap map[Port][]PortBinding 505 | err error 506 | ) 507 | 508 | portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236/tcp", "2345-2347/udp", "3456-3458/sctp"}) 509 | if err != nil { 510 | t.Fatalf("Error while processing ParsePortSpecs: %s", err) 511 | } 512 | 513 | if _, ok := portMap["1235/tcp"]; !ok { 514 | t.Fatal("1234/tcp was not parsed properly") 515 | } 516 | 517 | if _, ok := portMap["2346/udp"]; !ok { 518 | t.Fatal("2345/udp was not parsed properly") 519 | } 520 | 521 | if _, ok := portMap["3456/sctp"]; !ok { 522 | t.Fatal("3456/sctp was not parsed properly") 523 | } 524 | 525 | for portSpec, bindings := range bindingMap { 526 | if len(bindings) != 1 { 527 | t.Fatalf("%s should have exactly one binding", portSpec) 528 | } 529 | 530 | if bindings[0].HostIP != "" { 531 | t.Fatalf("HostIP should not be set for %s", portSpec) 532 | } 533 | 534 | if bindings[0].HostPort != "" { 535 | t.Fatalf("HostPort should not be set for %s", portSpec) 536 | } 537 | } 538 | 539 | portMap, bindingMap, err = ParsePortSpecs([]string{"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp", "3456-3458:3456-3458/sctp"}) 540 | if err != nil { 541 | t.Fatalf("Error while processing ParsePortSpecs: %s", err) 542 | } 543 | 544 | if _, ok := portMap["1235/tcp"]; !ok { 545 | t.Fatal("1234/tcp was not parsed properly") 546 | } 547 | 548 | if _, ok := portMap["2346/udp"]; !ok { 549 | t.Fatal("2345/udp was not parsed properly") 550 | } 551 | 552 | if _, ok := portMap["3456/sctp"]; !ok { 553 | t.Fatal("3456/sctp was not parsed properly") 554 | } 555 | 556 | for portSpec, bindings := range bindingMap { 557 | _, port := SplitProtoPort(string(portSpec)) 558 | if len(bindings) != 1 { 559 | t.Fatalf("%s should have exactly one binding", portSpec) 560 | } 561 | 562 | if bindings[0].HostIP != "" { 563 | t.Fatalf("HostIP should not be set for %s", portSpec) 564 | } 565 | 566 | if bindings[0].HostPort != port { 567 | t.Fatalf("HostPort should be %s for %s", port, portSpec) 568 | } 569 | } 570 | 571 | portMap, bindingMap, err = ParsePortSpecs([]string{"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp", "0.0.0.0:3456-3458:3456-3458/sctp"}) 572 | if err != nil { 573 | t.Fatalf("Error while processing ParsePortSpecs: %s", err) 574 | } 575 | 576 | if _, ok := portMap["1235/tcp"]; !ok { 577 | t.Fatal("1234/tcp was not parsed properly") 578 | } 579 | 580 | if _, ok := portMap["2346/udp"]; !ok { 581 | t.Fatal("2345/udp was not parsed properly") 582 | } 583 | 584 | if _, ok := portMap["3456/sctp"]; !ok { 585 | t.Fatal("3456/sctp was not parsed properly") 586 | } 587 | 588 | for portSpec, bindings := range bindingMap { 589 | _, port := SplitProtoPort(string(portSpec)) 590 | if len(bindings) != 1 || bindings[0].HostIP != "0.0.0.0" || bindings[0].HostPort != port { 591 | t.Fatalf("Expect single binding to port %s but found %s", port, bindings) 592 | } 593 | } 594 | 595 | _, _, err = ParsePortSpecs([]string{"localhost:1234-1236:1234-1236/tcp"}) 596 | 597 | if err == nil { 598 | t.Fatal("Received no error while trying to parse a hostname instead of ip") 599 | } 600 | } 601 | 602 | func TestParseNetworkOptsPrivateOnly(t *testing.T) { 603 | ports, bindings, err := ParsePortSpecs([]string{"192.168.1.100::80"}) 604 | if err != nil { 605 | t.Fatal(err) 606 | } 607 | if len(ports) != 1 { 608 | t.Logf("Expected 1 got %d", len(ports)) 609 | t.FailNow() 610 | } 611 | if len(bindings) != 1 { 612 | t.Logf("Expected 1 got %d", len(bindings)) 613 | t.FailNow() 614 | } 615 | for k := range ports { 616 | if k.Proto() != "tcp" { 617 | t.Logf("Expected tcp got %s", k.Proto()) 618 | t.Fail() 619 | } 620 | if k.Port() != "80" { 621 | t.Logf("Expected 80 got %s", k.Port()) 622 | t.Fail() 623 | } 624 | b, exists := bindings[k] 625 | if !exists { 626 | t.Log("Binding does not exist") 627 | t.FailNow() 628 | } 629 | if len(b) != 1 { 630 | t.Logf("Expected 1 got %d", len(b)) 631 | t.FailNow() 632 | } 633 | s := b[0] 634 | if s.HostPort != "" { 635 | t.Logf("Expected \"\" got %s", s.HostPort) 636 | t.Fail() 637 | } 638 | if s.HostIP != "192.168.1.100" { 639 | t.Fail() 640 | } 641 | } 642 | } 643 | 644 | func TestParseNetworkOptsPublic(t *testing.T) { 645 | ports, bindings, err := ParsePortSpecs([]string{"192.168.1.100:8080:80"}) 646 | if err != nil { 647 | t.Fatal(err) 648 | } 649 | if len(ports) != 1 { 650 | t.Logf("Expected 1 got %d", len(ports)) 651 | t.FailNow() 652 | } 653 | if len(bindings) != 1 { 654 | t.Logf("Expected 1 got %d", len(bindings)) 655 | t.FailNow() 656 | } 657 | for k := range ports { 658 | if k.Proto() != "tcp" { 659 | t.Logf("Expected tcp got %s", k.Proto()) 660 | t.Fail() 661 | } 662 | if k.Port() != "80" { 663 | t.Logf("Expected 80 got %s", k.Port()) 664 | t.Fail() 665 | } 666 | b, exists := bindings[k] 667 | if !exists { 668 | t.Log("Binding does not exist") 669 | t.FailNow() 670 | } 671 | if len(b) != 1 { 672 | t.Logf("Expected 1 got %d", len(b)) 673 | t.FailNow() 674 | } 675 | s := b[0] 676 | if s.HostPort != "8080" { 677 | t.Logf("Expected 8080 got %s", s.HostPort) 678 | t.Fail() 679 | } 680 | if s.HostIP != "192.168.1.100" { 681 | t.Fail() 682 | } 683 | } 684 | } 685 | 686 | func TestParseNetworkOptsPublicNoPort(t *testing.T) { 687 | ports, bindings, err := ParsePortSpecs([]string{"192.168.1.100"}) 688 | 689 | if err == nil { 690 | t.Logf("Expected error Invalid containerPort") 691 | t.Fail() 692 | } 693 | if ports != nil { 694 | t.Logf("Expected nil got %s", ports) 695 | t.Fail() 696 | } 697 | if bindings != nil { 698 | t.Logf("Expected nil got %s", bindings) 699 | t.Fail() 700 | } 701 | } 702 | 703 | func TestParseNetworkOptsNegativePorts(t *testing.T) { 704 | ports, bindings, err := ParsePortSpecs([]string{"192.168.1.100:-1:-1"}) 705 | 706 | if err == nil { 707 | t.Fail() 708 | } 709 | if len(ports) != 0 { 710 | t.Logf("Expected 0 got %d: %#v", len(ports), ports) 711 | t.Fail() 712 | } 713 | if len(bindings) != 0 { 714 | t.Logf("Expected 0 got %d: %#v", len(bindings), bindings) 715 | t.Fail() 716 | } 717 | } 718 | 719 | func TestParseNetworkOptsUdp(t *testing.T) { 720 | ports, bindings, err := ParsePortSpecs([]string{"192.168.1.100::6000/udp"}) 721 | if err != nil { 722 | t.Fatal(err) 723 | } 724 | if len(ports) != 1 { 725 | t.Logf("Expected 1 got %d: %#v", len(ports), ports) 726 | t.FailNow() 727 | } 728 | if len(bindings) != 1 { 729 | t.Logf("Expected 1 got %d: %#v", len(bindings), bindings) 730 | t.FailNow() 731 | } 732 | for k := range ports { 733 | if k.Proto() != "udp" { 734 | t.Logf("Expected udp got %s", k.Proto()) 735 | t.Fail() 736 | } 737 | if k.Port() != "6000" { 738 | t.Logf("Expected 6000 got %s", k.Port()) 739 | t.Fail() 740 | } 741 | b, exists := bindings[k] 742 | if !exists { 743 | t.Log("Binding does not exist") 744 | t.FailNow() 745 | } 746 | if len(b) != 1 { 747 | t.Logf("Expected 1 got %d", len(b)) 748 | t.FailNow() 749 | } 750 | s := b[0] 751 | if s.HostPort != "" { 752 | t.Logf("Expected \"\" got %s", s.HostPort) 753 | t.Fail() 754 | } 755 | if s.HostIP != "192.168.1.100" { 756 | t.Fail() 757 | } 758 | } 759 | } 760 | 761 | func TestParseNetworkOptsSctp(t *testing.T) { 762 | ports, bindings, err := ParsePortSpecs([]string{"192.168.1.100::6000/sctp"}) 763 | if err != nil { 764 | t.Fatal(err) 765 | } 766 | if len(ports) != 1 { 767 | t.Logf("Expected 1 got %d: %#v", len(ports), ports) 768 | t.FailNow() 769 | } 770 | if len(bindings) != 1 { 771 | t.Logf("Expected 1 got %d: %#v", len(bindings), bindings) 772 | t.FailNow() 773 | } 774 | for k := range ports { 775 | if k.Proto() != "sctp" { 776 | t.Logf("Expected sctp got %s", k.Proto()) 777 | t.Fail() 778 | } 779 | if k.Port() != "6000" { 780 | t.Logf("Expected 6000 got %s", k.Port()) 781 | t.Fail() 782 | } 783 | b, exists := bindings[k] 784 | if !exists { 785 | t.Log("Binding does not exist") 786 | t.FailNow() 787 | } 788 | if len(b) != 1 { 789 | t.Logf("Expected 1 got %d", len(b)) 790 | t.FailNow() 791 | } 792 | s := b[0] 793 | if s.HostPort != "" { 794 | t.Logf("Expected \"\" got %s", s.HostPort) 795 | t.Fail() 796 | } 797 | if s.HostIP != "192.168.1.100" { 798 | t.Fail() 799 | } 800 | } 801 | } 802 | 803 | func TestStringer(t *testing.T) { 804 | tests := []struct { 805 | doc string 806 | in string 807 | expected string 808 | }{ 809 | { 810 | doc: "no host mapping", 811 | in: ":8080:6000/tcp", 812 | expected: ":8080:6000/tcp", 813 | }, 814 | { 815 | doc: "no proto", 816 | in: "192.168.1.100:8080:6000", 817 | expected: "192.168.1.100:8080:6000/tcp", 818 | }, 819 | { 820 | doc: "no host port", 821 | in: "192.168.1.100::6000/udp", 822 | expected: "192.168.1.100::6000/udp", 823 | }, 824 | { 825 | doc: "no mapping, port, or proto", 826 | in: "::6000", 827 | expected: "::6000/tcp", 828 | }, 829 | { 830 | doc: "ipv4 mapping", 831 | in: "192.168.1.100:8080:6000/udp", 832 | expected: "192.168.1.100:8080:6000/udp", 833 | }, 834 | { 835 | doc: "ipv4 mapping without host port", 836 | in: "192.168.1.100::6000/udp", 837 | expected: "192.168.1.100::6000/udp", 838 | }, 839 | { 840 | doc: "ipv6 mapping", 841 | in: "[::1]:8080:6000/udp", 842 | expected: "[::1]:8080:6000/udp", 843 | }, 844 | { 845 | doc: "ipv6 mapping without host port", 846 | in: "[::1]::6000/udp", 847 | expected: "[::1]::6000/udp", 848 | }, 849 | { 850 | doc: "ipv6 legacy mapping", 851 | in: "::1:8080:6000/udp", 852 | expected: "[::1]:8080:6000/udp", 853 | }, 854 | { 855 | doc: "ipv6 legacy mapping without host port", 856 | in: "::::6000/udp", 857 | expected: "[::]::6000/udp", 858 | }, 859 | } 860 | for _, tc := range tests { 861 | t.Run(tc.doc, func(t *testing.T) { 862 | mappings, err := ParsePortSpec(tc.in) 863 | if err != nil { 864 | t.Fatal(err) 865 | } 866 | if len(mappings) != 1 { 867 | // All tests produce a single mapping 868 | t.Fatalf("Expected 1 got %d", len(mappings)) 869 | } 870 | if actual := mappings[0].String(); actual != tc.expected { 871 | t.Errorf("Expected %s got %s", tc.expected, actual) 872 | } 873 | }) 874 | } 875 | } 876 | 877 | func BenchmarkParsePortSpecs(b *testing.B) { 878 | specs := [][]string{ 879 | {"1234/tcp", "2345/udp", "3456/sctp"}, 880 | {"1234:1234/tcp", "2345:2345/udp", "3456:3456/sctp"}, 881 | {"0.0.0.0:1234:1234/tcp", "0.0.0.0:2345:2345/udp", "0.0.0.0:3456:3456/sctp"}, 882 | {"1234-1236/tcp", "2345-2347/udp", "3456-3458/sctp"}, 883 | {"1234-1236:1234-1236/tcp", "2345-2347:2345-2347/udp", "3456-3458:3456-3458/sctp"}, 884 | {"0.0.0.0:1234-1236:1234-1236/tcp", "0.0.0.0:2345-2347:2345-2347/udp", "0.0.0.0:3456-3458:3456-3458/sctp"}, 885 | {"[2001:4860:0:2001::68]::333"}, 886 | {"[::1]:80:80"}, 887 | {"::1:80:80"}, 888 | {"::::80"}, 889 | } 890 | 891 | b.ReportAllocs() 892 | b.ResetTimer() 893 | for i := 0; i < b.N; i++ { 894 | for _, group := range specs { 895 | if _, _, err := ParsePortSpecs(group); err != nil { 896 | b.Fatalf("unexpected error: %v", err) 897 | } 898 | } 899 | } 900 | } 901 | --------------------------------------------------------------------------------