pax_global_header00006660000000000000000000000064144726231440014521gustar00rootroot0000000000000052 comment=51738be2e63908a0fb411cf993ba7cdf7b5e73a3 snid-0.3.0/000077500000000000000000000000001447262314400124565ustar00rootroot00000000000000snid-0.3.0/.github/000077500000000000000000000000001447262314400140165ustar00rootroot00000000000000snid-0.3.0/.github/workflows/000077500000000000000000000000001447262314400160535ustar00rootroot00000000000000snid-0.3.0/.github/workflows/release.yml000066400000000000000000000052141447262314400202200ustar00rootroot00000000000000on: release: types: [published] name: Publish Release Binaries jobs: release: name: Build and Upload Release Binaries runs-on: ubuntu-latest permissions: contents: write steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: 1.x - name: Build binaries run: | GOPATH=${{ runner.temp }}/go CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install src.agwa.name/snid/...@${{ github.event.release.name }} GOPATH=${{ runner.temp }}/go CGO_ENABLED=0 GOOS=linux GOARCH=arm go install src.agwa.name/snid/...@${{ github.event.release.name }} GOPATH=${{ runner.temp }}/go CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go install src.agwa.name/snid/...@${{ github.event.release.name }} GOPATH=${{ runner.temp }}/go CGO_ENABLED=0 GOOS=linux GOARCH=386 go install src.agwa.name/snid/...@${{ github.event.release.name }} - name: Upload binaries uses: actions/github-script@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const fs = require("fs").promises; const { repo: { owner, repo }, sha } = context; await github.repos.uploadReleaseAsset({ owner, repo, release_id: ${{ github.event.release.id }}, name: "snid-${{ github.event.release.name }}-linux-amd64", data: await fs.readFile("${{ runner.temp }}/go/bin/snid"), }); await github.repos.uploadReleaseAsset({ owner, repo, release_id: ${{ github.event.release.id }}, name: "snid-${{ github.event.release.name }}-linux-arm", data: await fs.readFile("${{ runner.temp }}/go/bin/linux_arm/snid"), }); await github.repos.uploadReleaseAsset({ owner, repo, release_id: ${{ github.event.release.id }}, name: "snid-${{ github.event.release.name }}-linux-arm64", data: await fs.readFile("${{ runner.temp }}/go/bin/linux_arm64/snid"), }); await github.repos.uploadReleaseAsset({ owner, repo, release_id: ${{ github.event.release.id }}, name: "snid-${{ github.event.release.name }}-linux-386", data: await fs.readFile("${{ runner.temp }}/go/bin/linux_386/snid"), }); await github.repos.uploadReleaseAsset({ owner, repo, release_id: ${{ github.event.release.id }}, name: "sum.golang.org-sth", data: await fs.readFile("${{ runner.temp }}/go/pkg/sumdb/sum.golang.org/latest"), }); snid-0.3.0/.gitignore000066400000000000000000000000061447262314400144420ustar00rootroot00000000000000/snid snid-0.3.0/LICENSE000066400000000000000000000023361447262314400134670ustar00rootroot00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Except as contained in this notice, the name(s) of the above copyright holders shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Software without prior written authorization. snid-0.3.0/README.md000066400000000000000000000131761447262314400137450ustar00rootroot00000000000000# snid - SNI-based Proxy Server snid is a lightweight proxy server that forwards TLS connections based on the server name indication (SNI) hostname. snid favors convention over configuration - backend addresses are not configured individually, but rather constructed based on DNS record lookups or filesystem locations. This makes snid deployments easy to manage. ## Installing snid If you have the latest version of Go installed, you can run: ``` go install src.agwa.name/snid@latest ``` Or, you can download a binary from the [GitHub Releases Page](https://github.com/AGWA/snid/releases). snid is a single statically-linked binary so using Docker or a similar technology is superfluous. ## Command Line Arguments ### `-listen LISTENER` (Mandatory) Listen on the given address, provided in [go-listener syntax](https://pkg.go.dev/src.agwa.name/go-listener#readme-listener-syntax). You can specify the `-listen` flag multiple times to listen on multiple addresses. Examples: * `-listen tcp:443` to listen on TCP port 443, all interfaces. * `-listen tcp:0.0.0.0:443` to listen on TCP port 443, all IPv4 interfaces. * `-listen tcp:192.0.2.4:443` to listen on TCP port 443 on 192.0.2.4. ### `-mode nat46`, `-mode tcp`, or `-mode unix` (Mandatory) Use the given mode, described below. ### `-default-hostname HOSTNAME` (Optional) Use the given hostname if a client does not include the SNI extension. If this flag is not specified, then SNI-less connections will be terminated with a TLS alert. ## NAT46 mode In NAT46 mode, snid does a DNS lookup on the SNI hostname to determine its IPv6 address and forwards the connection there, as long as the IPv6 address is within one of the networks specified by `-backend-cidr`. The client's IPv4 address is embedded in the lower 4 bytes of the source address used for connecting to the backend, with the prefix specified by `-nat46-prefix`. Note: in NAT46 mode, clients which connect to snid over IPv6 will be disconnected. Instead, IPv6 clients should connect directly to the backend. The following flags can be specified in NAT46 mode: ### `-nat46-prefix IPV6ADDRESS` (Mandatory) Use the given prefix for the source address when connecting to the backend. Specifically, the source address is constructed by taking the IPv6 address specified by `-nat46-prefix` and placing the client's IPv4 address in the lower 4 bytes. It is recommended that you use one of the prefixes reserved by [RFC 8215](https://datatracker.ietf.org/doc/html/rfc8215) for IPv4/IPv6 translation mechanisms, such as `64:ff9b:1::`. Example: `-nat46-prefix 64:ff9b:1::` Important: the prefix which you use for `-nat46-prefix` MUST be routed to the local host so that return packets can reach snid. On Linux, the necessary route entry can be added by running: ``` ip route add local 64:ff9b:1::/96 dev lo ``` ### `-backend-cidr CIDR` (Mandatory) Only forward connections to addresses within the given subnet. This option can be specified multiple times to allow multiple subnets. Example: `-backend-cidr 2001:db8::/64` ## TCP mode In TCP mode, snid does a DNS record lookup on the SNI hostname to determine its IPv4 or IPv6 address and forwards the connection there, as long as the IP address is within one of the networks specified by `-backend-cidr`. The following flags can be specified in TCP mode: ### `-backend-cidr CIDR` (Mandatory) Only forward connections to addresses within the given subnet. This option can be specified multiple times to allow multiple subnets. Examples: * `-backend-cidr 192.0.2.0/24` * `-backend-cidr 2001:db8::/64` ### `-backend-port PORTNO` (Optional) Connect to the given port number on the backend. If this option is omitted, then snid will use the same port number that the inbound connection arrived on. ### `-proxy-proto` (Optional) Use [PROXY protocol v2](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to convey the client IP address to the backend. ## UNIX mode In UNIX mode, snid forwards connections to a UNIX domain socket whose filename is the SNI hostname, in the directory specified by `-unix-directory`. The following flags can be specified with UNIX mode: ### `-unix-directory PATH` (Mandatory) The path to the directory containing UNIX domain sockets. ### `-proxy-proto` (Optional) Use [PROXY protocol v2](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) to convey the client IP address to the backend. ## DNS Lookup Behavior In NAT46 and TCP modes, snid does a DNS lookup on the SNI hostname to determine the backend's IP address. snid attempts to emulate the DNS lookup behavior that a TLS client would use if connecting directly to the backend. Normally, snid does an A/AAAA record lookup directly on the hostname, but if the TLS handshake specifies exactly one ALPN value for a protocol which uses SRV records, then snid will do a SRV record lookup instead. The following ALPN values are recognized: | Sole ALPN Value | SRV Service | | ------------------ | ----------------------- | | `xmpp-client` | `_xmpps-client._tcp` | | `xmpp-server` | `_xmpps-server._tcp` | For example, if the handshake specifies the SNI hostname `example.com` and the ALPN protcols `h2` and `http/1.1`, then snid will look up the A/AAAA records for `example.com` and forward the connection there, since that's how an HTTP client works. If the handshake specifies the SNI hostname `example.com` and the ALPN protcol `xmpp-client`, then snid will do a SRV record lookup for `_xmpps-client._tcp.example.com`'. If this returns a SRV record for `xmpp.example.com`, then snid will look up the A/AAAA records for `xmpp.example.com` and forward the connection there, since that's how an XMPP client works. snid-0.3.0/backend.go000066400000000000000000000031101447262314400143670ustar00rootroot00000000000000// Copyright (C) 2022 Andrew Ayer // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Except as contained in this notice, the name(s) of the above copyright // holders shall not be used in advertising or otherwise to promote the // sale, use or other dealings in this Software without prior written // authorization. package main import ( "net" ) type BackendConn interface { net.Conn CloseWrite() error } type ClientConn interface { LocalAddr() net.Addr RemoteAddr() net.Addr } type BackendDialer interface { Dial(string, []string, ClientConn) (BackendConn, error) } snid-0.3.0/go.mod000066400000000000000000000001151447262314400135610ustar00rootroot00000000000000module src.agwa.name/snid go 1.20 require src.agwa.name/go-listener v0.5.0 snid-0.3.0/go.sum000066400000000000000000000015151447262314400136130ustar00rootroot00000000000000src.agwa.name/go-listener v0.2.0 h1:HZHMLbwjXle8ZN97SkTgAIi4nsp08aEDq4uY7D0EU8Y= src.agwa.name/go-listener v0.2.0/go.mod h1:jitkAgaNrvQ/EFaVPO/aIJytWYdF6Bk2Gmbbm8hw14Y= src.agwa.name/go-listener v0.3.0 h1:DY0df9lMiknYjpfD2GYqhG07SCnyOiIkQaPd2yUC2tY= src.agwa.name/go-listener v0.3.0/go.mod h1:naXgwyLIMwmT0rPc3FWeyvKtOcHcpV8FW4Be3rB7zjw= src.agwa.name/go-listener v0.3.1 h1:LBQNeXfMor/HDfa4frmurzOLBlGDmp3GkPYTicY0Drg= src.agwa.name/go-listener v0.3.1/go.mod h1:naXgwyLIMwmT0rPc3FWeyvKtOcHcpV8FW4Be3rB7zjw= src.agwa.name/go-listener v0.4.0 h1:h5LPiKh2YUFjuer+2G6bUDZBFTrFM5j9St0gLyFh55s= src.agwa.name/go-listener v0.4.0/go.mod h1:rpJv/8nIn70oIgbkm21MDSc6NgzzcWa8RYwXfMWn6kg= src.agwa.name/go-listener v0.5.0 h1:UjXAdtPKO1o1T/6B25TlpJaG/EJao+UvUEpRZuGy8iA= src.agwa.name/go-listener v0.5.0/go.mod h1:vM7zjlskRnSbxbS8QTWYQIOfuKsn8SdJQH28RtouDTI= snid-0.3.0/hostname.go000066400000000000000000000037111447262314400146250ustar00rootroot00000000000000// Copyright (C) 2022 Andrew Ayer // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Except as contained in this notice, the name(s) of the above copyright // holders shall not be used in advertising or otherwise to promote the // sale, use or other dealings in this Software without prior written // authorization. package main import ( "errors" "strings" ) func replaceFirstLabel(hostname string, replacement string) string { dot := strings.IndexByte(hostname, '.') if dot == -1 { return replacement } else { return replacement + hostname[dot:] } } func canonicalizeHostname(hostname string) (string, error) { if len(hostname) == 0 || hostname[0] == '.' || strings.IndexByte(hostname, '/') >= 0 { return "", errors.New("invalid hostname") } hostname = strings.ToLower(hostname) hostname = strings.TrimSuffix(hostname, ".") return hostname, nil } func wildcardHostname(hostname string) string { return replaceFirstLabel(hostname, "_") } snid-0.3.0/main.go000066400000000000000000000106371447262314400137400ustar00rootroot00000000000000// Copyright (C) 2022 Andrew Ayer // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Except as contained in this notice, the name(s) of the above copyright // holders shall not be used in advertising or otherwise to promote the // sale, use or other dealings in this Software without prior written // authorization. package main import ( "flag" "fmt" "log" "net" "src.agwa.name/go-listener" ) func main() { var flags struct { listen []string defaultHostname string mode string proxyProto bool unixDirectory string backendCidr []*net.IPNet backendPort int nat46Prefix net.IP } flag.Func("listen", "Socket to listen on (repeatable)", func(arg string) error { flags.listen = append(flags.listen, arg) return nil }) flag.StringVar(&flags.defaultHostname, "default-hostname", "", "Default hostname if client does not provide SNI") flag.StringVar(&flags.mode, "mode", "", "unix, tcp, or nat46") flag.BoolVar(&flags.proxyProto, "proxy-proto", false, "Use PROXY protocol when talking to backend (tcp, unix modes)") flag.StringVar(&flags.unixDirectory, "unix-directory", "", "Path to directory containing backend UNIX sockets (unix mode)") flag.Func("backend-cidr", "CIDR of allowed backends (repeatable) (tcp, nat46 modes)", func(arg string) error { _, ipnet, err := net.ParseCIDR(arg) if err != nil { return err } flags.backendCidr = append(flags.backendCidr, ipnet) return nil }) flag.IntVar(&flags.backendPort, "backend-port", 0, "Port number of backend (defaults to same port number as listener) (tcp mode)") flag.Func("nat46-prefix", "IPv6 prefix for NAT46 source address (nat46 mode)", func(arg string) error { flags.nat46Prefix = net.ParseIP(arg) if flags.nat46Prefix == nil { return fmt.Errorf("not a valid IP address") } if flags.nat46Prefix.To4() != nil { return fmt.Errorf("not an IPv6 address") } return nil }) flag.Parse() server := &Server{ ProxyProtocol: flags.proxyProto, DefaultHostname: flags.defaultHostname, } switch flags.mode { case "unix": if flags.unixDirectory == "" { log.Fatal("-unix-directory must be specified when you use -mode unix") } server.Backend = &UnixDialer{Directory: flags.unixDirectory} case "tcp": if len(flags.backendCidr) == 0 { log.Fatal("At least one -backend-cidr flag must be specified when you use -mode tcp") } server.Backend = &TCPDialer{Port: flags.backendPort, Allowed: flags.backendCidr} case "nat46": if flags.proxyProto { log.Fatal("-proxy-proto must not be specified when you use -mode nat46") } if flags.backendPort != 0 { log.Fatal("-backend-port must not be specified when you use -mode nat46") } if len(flags.backendCidr) == 0 { log.Fatal("At least one -backend-cidr flag must be specified when you use -mode nat46") } if flags.nat46Prefix == nil { log.Fatal("-nat46-prefix must be specified when you use -mode nat46") } server.Backend = &TCPDialer{Allowed: flags.backendCidr, IPv6SourcePrefix: flags.nat46Prefix} default: log.Fatal("-mode must be unix, tcp, or nat46") } if len(flags.listen) == 0 { log.Fatal("At least one -listen flag must be specified") } listeners, err := listener.OpenAll(flags.listen) if err != nil { log.Fatal(err) } defer listener.CloseAll(listeners) for _, l := range listeners { go serve(l, server) } select {} } func serve(listener net.Listener, server *Server) { log.Fatal(server.Serve(listener)) } snid-0.3.0/server.go000066400000000000000000000071651447262314400143240ustar00rootroot00000000000000// Copyright (C) 2022 Andrew Ayer // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Except as contained in this notice, the name(s) of the above copyright // holders shall not be used in advertising or otherwise to promote the // sale, use or other dealings in this Software without prior written // authorization. package main import ( "crypto/tls" "errors" "io" "log" "net" "time" "src.agwa.name/go-listener/proxy" "src.agwa.name/go-listener/tlsutil" ) type Server struct { Backend BackendDialer ProxyProtocol bool DefaultHostname string } func (server *Server) peekClientHello(clientConn net.Conn) (*tls.ClientHelloInfo, net.Conn, error) { if err := clientConn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil { return nil, nil, err } clientHello, peekedClientConn, err := tlsutil.PeekClientHelloFromConn(clientConn) if err != nil { return nil, nil, err } if err := clientConn.SetReadDeadline(time.Time{}); err != nil { return nil, nil, err } if clientHello.ServerName == "" { if server.DefaultHostname == "" { return nil, nil, errors.New("no SNI provided and DefaultHostname not set") } clientHello.ServerName = server.DefaultHostname } return clientHello, peekedClientConn, err } func (server *Server) handleConnection(clientConn net.Conn) { defer func() { clientConn.Close() }() var clientHello *tls.ClientHelloInfo if peekedClientHello, peekedClientConn, err := server.peekClientHello(clientConn); err == nil { clientHello = peekedClientHello clientConn = peekedClientConn } else { log.Printf("Peeking client hello from %s failed: %s", clientConn.RemoteAddr(), err) return } backendConn, err := server.Backend.Dial(clientHello.ServerName, clientHello.SupportedProtos, clientConn) if err != nil { log.Printf("Ignoring connection from %s because dialing backend failed: %s", clientConn.RemoteAddr(), err) return } defer backendConn.Close() if server.ProxyProtocol { header := proxy.Header{RemoteAddr: clientConn.RemoteAddr(), LocalAddr: clientConn.LocalAddr()} if _, err := backendConn.Write(header.Format()); err != nil { log.Printf("Error writing PROXY header to backend: %s", err) return } } go func() { io.Copy(backendConn, clientConn) backendConn.CloseWrite() }() io.Copy(clientConn, backendConn) } func (server *Server) Serve(listener net.Listener) error { for { conn, err := listener.Accept() if err != nil { if netErr, isNetErr := err.(net.Error); isNetErr && netErr.Temporary() { log.Printf("Temporary network error accepting connection: %s", netErr) continue } return err } go server.handleConnection(conn) } } snid-0.3.0/srv.go000066400000000000000000000015221447262314400136170ustar00rootroot00000000000000package main import ( "errors" "fmt" "net" "strconv" ) func getSRVService(protocols []string) string { if len(protocols) == 0 { return "" } switch protocols[0] { case "xmpp-client": return "xmpps-client" case "xmpp-server": return "xmpps-server" } return "" } func dialSRV(dialer net.Dialer, network string, hostname string, service string) (net.Conn, error) { _, addrs, err := net.LookupSRV(service, "tcp", hostname) if err != nil { return nil, err } if len(addrs) == 0 { return nil, fmt.Errorf("no SRV records exist for %s on %s", service, hostname) } var errs []error for _, addr := range addrs { conn, err := dialer.Dial(network, net.JoinHostPort(addr.Target, strconv.FormatUint(uint64(addr.Port), 10))) if err == nil { return conn, nil } errs = append(errs, err) } return nil, errors.Join(errs...) } snid-0.3.0/tcp.go000066400000000000000000000100311447262314400135660ustar00rootroot00000000000000// Copyright (C) 2022 Andrew Ayer // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Except as contained in this notice, the name(s) of the above copyright // holders shall not be used in advertising or otherwise to promote the // sale, use or other dealings in this Software without prior written // authorization. package main import ( "fmt" "net" "strconv" "syscall" "time" ) type TCPDialer struct { Port int Allowed []*net.IPNet IPv6SourcePrefix net.IP } func (backend *TCPDialer) checkBackend(address string) error { host, _, err := net.SplitHostPort(address) if err != nil { return err } ipaddress := net.ParseIP(host) if ipaddress == nil { return fmt.Errorf("%s is not a valid IP address", host) } for _, cidr := range backend.Allowed { if cidr.Contains(ipaddress) { return nil } } return fmt.Errorf("%s is not an allowed backend", ipaddress) } func (backend *TCPDialer) bindIPv6(sock syscall.RawConn, clientConn ClientConn) error { clientTCPAddress, isTCP := clientConn.RemoteAddr().(*net.TCPAddr) if !isTCP { return fmt.Errorf("client is not connected using TCP") } clientIPv4 := clientTCPAddress.IP.To4() if clientIPv4 == nil { return fmt.Errorf("client is not connected using IPv4") } sourceIPv6 := make(net.IP, 16) copy(sourceIPv6[:12], backend.IPv6SourcePrefix) copy(sourceIPv6[12:], clientIPv4) var controlErr error if err := sock.Control(func(fd uintptr) { controlErr = syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_FREEBIND, 1) if controlErr != nil { return } controlErr = syscall.Bind(int(fd), &syscall.SockaddrInet6{Addr: *(*[16]byte)(sourceIPv6)}) }); err != nil { return err } return controlErr } func (backend *TCPDialer) port(clientConn ClientConn) (int, error) { if backend.Port != 0 { return backend.Port, nil } localTCPAddress, isTCP := clientConn.LocalAddr().(*net.TCPAddr) if !isTCP { return 0, fmt.Errorf("cannot determine backend port number because client is not connected using TCP") } return localTCPAddress.Port, nil } func (backend *TCPDialer) network() string { if backend.IPv6SourcePrefix != nil { return "tcp6" } else { return "tcp" } } func (backend *TCPDialer) Dial(hostname string, protocols []string, clientConn ClientConn) (BackendConn, error) { dialer := net.Dialer{ Timeout: 5 * time.Second, Control: func(network string, address string, c syscall.RawConn) error { if err := backend.checkBackend(address); err != nil { return err } if backend.IPv6SourcePrefix != nil { if err := backend.bindIPv6(c, clientConn); err != nil { return err } } return nil }, } if service := getSRVService(protocols); service != "" { conn, err := dialSRV(dialer, backend.network(), hostname, service) if err != nil { return nil, err } return conn.(*net.TCPConn), nil } port, err := backend.port(clientConn) if err != nil { return nil, err } conn, err := dialer.Dial(backend.network(), net.JoinHostPort(hostname, strconv.Itoa(port))) if err != nil { return nil, err } return conn.(*net.TCPConn), nil } snid-0.3.0/unix.go000066400000000000000000000044051447262314400137730ustar00rootroot00000000000000// Copyright (C) 2022 Andrew Ayer // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR // OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // Except as contained in this notice, the name(s) of the above copyright // holders shall not be used in advertising or otherwise to promote the // sale, use or other dealings in this Software without prior written // authorization. package main import ( "errors" "fmt" "io/fs" "net" "path/filepath" ) type UnixDialer struct { Directory string } func (backend *UnixDialer) Dial(origHostname string, protocols []string, clientConn ClientConn) (BackendConn, error) { hostname, err := canonicalizeHostname(origHostname) if err != nil { return nil, fmt.Errorf("invalid hostname %q", origHostname) } if conn, err := backend.dial(hostname); err == nil { return conn, nil } else if !errors.Is(err, fs.ErrNotExist) { return nil, err } if conn, err := backend.dial(wildcardHostname(hostname)); err == nil { return conn, nil } else if !errors.Is(err, fs.ErrNotExist) { return nil, err } return nil, fmt.Errorf("no backend socket found for %q", hostname) } func (backend *UnixDialer) dial(socketName string) (BackendConn, error) { socketPath := filepath.Join(backend.Directory, socketName) return net.DialUnix("unix", nil, &net.UnixAddr{Net: "unix", Name: socketPath}) }