From e2b3ea8a80f9ec103b899723b544ac439179cbf1 Mon Sep 17 00:00:00 2001 From: Mikhail Knyazhev Date: Sat, 29 Mar 2025 06:29:56 +0300 Subject: [PATCH] refactoring --- .github/workflows/ci.yml | 2 +- .gitignore | 37 +++-- .golangci.yml | 293 ++++++++-------------------------- LICENSE | 2 +- Makefile | 32 ++-- address/common.go | 94 ++++++++--- address/common_test.go | 47 +++++- client/adapter.go | 36 +++++ client/client.go | 247 ++++++++-------------------- client/config.go | 37 +++++ client/log.go | 10 -- client/pool.go | 93 ----------- client/tls.go | 49 +++++- epoll/common.go | 2 +- epoll/epoll.go | 5 +- epoll/epoll_net.go | 2 +- epoll/options.go | 2 +- epoll/server_tcp.go | 7 +- errs/errs.go | 31 ++++ examples/client/main.go | 53 +++--- examples/epoll-server/main.go | 5 +- examples/server/main.go | 37 +++-- fd/fd.go | 2 +- go.mod | 25 +-- go.sum | 58 ++++--- internal/datapool.go | 15 ++ internal/deadline.go | 40 +++++ internal/errors.go | 17 +- internal/network.go | 18 +-- internal/packet.go | 39 +++++ internal/packet_test.go | 37 +++++ internal/times.go | 32 ---- internal/tls.go | 13 +- listen/listener.go | 36 ++--- listen/tls.go | 39 +++-- server/common.go | 19 --- server/config.go | 20 +++ server/prc.go | 77 --------- server/rwc.go | 96 ----------- server/server.go | 196 ++++++++--------------- 40 files changed, 836 insertions(+), 1066 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 .golangci.yml mode change 100644 => 100755 Makefile create mode 100644 client/adapter.go create mode 100644 client/config.go delete mode 100644 client/log.go delete mode 100644 client/pool.go create mode 100644 errs/errs.go create mode 100644 internal/datapool.go create mode 100644 internal/deadline.go create mode 100644 internal/packet.go create mode 100644 internal/packet_test.go delete mode 100644 internal/times.go delete mode 100644 server/common.go create mode 100644 server/config.go delete mode 100644 server/prc.go delete mode 100644 server/rwc.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d56821..bea49eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go: [ '1.22.10' ] + go: [ '1.23.6' ] steps: - uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 37e78f6..36e3bee --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,38 @@ + +.tools/ + +bin/ + +vendor/ + +build/ + +.idea/ + +.vscode/ + +coverage.txt + +coverage.out + *.exe + *.exe~ + *.dll + *.so + *.dylib + *.db + *.db-journal + *.mmdb -*.test -*.out -.idea/ -.vscode/ -.tools/ +*.test -coverage.txt -coverage.out +*.out -bin/ -vendor/ -build/ +.env diff --git a/.golangci.yml b/.golangci.yml old mode 100644 new mode 100755 index 91f307f..9c6a954 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,180 +1,87 @@ -# options for analysis running run: - # timeout for analysis, e.g. 30s, 5m, default is 1m - deadline: 5m - - # exit code when at least one issue was found, default is 1 + go: "1.23.6" + concurrency: 4 + timeout: 5m + tests: false issues-exit-code: 1 - - # include test files or not, default is true - tests: true - - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - skip-files: - - easyjson + modules-download-mode: readonly issues: - # Independently from option 'exclude' we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute 'golangci-lint run --help'. - # Default value for this option is true. exclude-use-default: false - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - prealloc - - errcheck + max-issues-per-linter: 100 + max-same-issues: 4 + new: false + exclude-files: + - ".+_test.go" + exclude-dirs: + - "vendor$" -# output configuration options output: - # colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number" - format: colored-line-number + formats: + - format: line-number + sort-results: true - # print lines of code with issue, default is true - print-issued-lines: true - - # print linter name in the end of issue text, default is true - print-linter-name: true - -# all available settings of specific linters linters-settings: govet: - # report about shadowed variables check-shadowing: true - enable: - # report mismatches between assembly files and Go declarations - - asmdecl - # check for useless assignments - - assign - # check for common mistakes using the sync/atomic package - - atomic - # check for non-64-bits-aligned arguments to sync/atomic functions - - atomicalign - # check for common mistakes involving boolean operators - - bools - # check that +build tags are well-formed and correctly located - - buildtag - # detect some violations of the cgo pointer passing rules - - cgocall - # check for unkeyed composite literals - - composites - # check for locks erroneously passed by value - - copylocks - # check for calls of reflect.DeepEqual on error values - - deepequalerrors - # report passing non-pointer or non-error values to errors.As - - errorsas - # find calls to a particular function - - findcall - # report assembly that clobbers the frame pointer before saving it - - framepointer - # check for mistakes using HTTP responses - - httpresponse - # detect impossible interface-to-interface type assertions - - ifaceassert - # check references to loop variables from within nested functions - - loopclosure - # check cancel func returned by context.WithCancel is called - - lostcancel - # check for useless comparisons between functions and nil - - nilfunc - # check for redundant or impossible nil comparisons - - nilness - # check consistency of Printf format strings and arguments - - printf - # check for comparing reflect.Value values with == or reflect.DeepEqual - - reflectvaluecompare - # check for possible unintended shadowing of variables - - shadow - # check for shifts that equal or exceed the width of the integer - - shift - # check for unbuffered channel of os.Signal - - sigchanyzer - # check the argument type of sort.Slice - - sortslice - # check signature of methods of well-known interfaces - - stdmethods - # check for string(int) conversions - - stringintconv - # check that struct field tags conform to reflect.StructTag.Get - - structtag - # report calls to (*testing.T).Fatal from goroutines started by a test. - - testinggoroutine - # check for common mistaken usages of tests and examples - - tests - # report passing non-pointer or non-interface values to unmarshal - - unmarshal - # check for unreachable code - - unreachable - # check for invalid conversions of uintptr to unsafe.Pointer - - unsafeptr - # check for unused results of calls to some functions - - unusedresult - # checks for unused writes - - unusedwrite - disable: - # find structs that would use less memory if their fields were sorted - - fieldalignment + enable: + - asmdecl + - assign + - atomic + - atomicalign + - bools + - buildtag + - cgocall + - composites + - copylocks + - deepequalerrors + - errorsas + - findcall + - framepointer + - httpresponse + - ifaceassert + - loopclosure + - lostcancel + - nilfunc + - nilness + - printf + - reflectvaluecompare + - shadow + - shift + - sigchanyzer + - sortslice + - stdmethods + - stringintconv + - structtag + - testinggoroutine + - tests + - unmarshal + - unreachable + - unsafeptr + - unusedresult + - unusedwrite + disable: + - fieldalignment gofmt: - # simplify code: gofmt with '-s' option, true by default simplify: true errcheck: - # report about not checking of errors in type assetions: 'a := b.(MyStruct)'; - # default is false: such cases aren't reported by default. check-type-assertions: true - # report about assignment of errors to blank identifier: 'num, _ := strconv.Atoi(numStr)'; - # default is false: such cases aren't reported by default. check-blank: true gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 15 + min-complexity: 30 misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. locale: US prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: true # Report preallocation suggestions on for loops, false by default + range-loops: true + for-loops: true unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. check-exported: false gci: - # Section configuration to compare against. - # Section names are case-insensitive and may contain parameters in (). - # The default order of sections is 'standard > default > custom > blank > dot', - # If 'custom-order' is 'true', it follows the order of 'sections' option. - # Default: ["standard", "default"] - #sections: - #- standard # Standard section: captures all standard packages. - #- default # Default section: contains all imports that could not be matched to another section type. - #- blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. - #- dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. - # Skip generated files. - # Default: true skip-generated: true - # Enable custom order of sections. - # If 'true', make the section order the same as the order of 'sections'. - # Default: false custom-order: false gosec: - # To select a subset of rules to run. - # Available rules: https://github.com/securego/gosec#available-rules - # Default: [] - means include all rules includes: - G101 # Look for hard coded credentials - G102 # Bind to all interfaces @@ -210,9 +117,6 @@ linters-settings: - G504 # Import blocklist: net/http/cgi - G505 # Import blocklist: crypto/sha1 - G601 # Implicit memory aliasing of items from a range statement - # To specify a set of rules to explicitly exclude. - # Available rules: https://github.com/securego/gosec#available-rules - # Default: [] excludes: - G101 # Look for hard coded credentials - G102 # Bind to all interfaces @@ -248,104 +152,39 @@ linters-settings: - G504 # Import blocklist: net/http/cgi - G505 # Import blocklist: crypto/sha1 - G601 # Implicit memory aliasing of items from a range statement - # Exclude generated files - # Default: false exclude-generated: true - # Filter out the issues with a lower severity than the given value. - # Valid options are: low, medium, high. - # Default: low severity: medium - # Filter out the issues with a lower confidence than the given value. - # Valid options are: low, medium, high. - # Default: low confidence: medium - # Concurrency value. - # Default: the number of logical CPUs usable by the current process. concurrency: 12 - # To specify the configuration of rules. config: - # Globals are applicable to all rules. global: - # If true, ignore #nosec in comments (and an alternative as well). - # Default: false nosec: true - # Add an alternative comment prefix to #nosec (both will work at the same time). - # Default: "" "#nosec": "#my-custom-nosec" - # Define whether nosec issues are counted as finding or not. - # Default: false show-ignored: true - # Audit mode enables addition checks that for normal code analysis might be too nosy. - # Default: false audit: true G101: - # Regexp pattern for variables and constants to find. - # Default: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" - pattern: "(?i)example" - # If true, complain about all cases (even with low entropy). - # Default: false + pattern: "(?i)passwd|pass|password|pwd|secret|token|pw|apiKey|bearer|cred" ignore_entropy: false - # Maximum allowed entropy of the string. - # Default: "80.0" entropy_threshold: "80.0" - # Maximum allowed value of entropy/string length. - # Is taken into account if entropy >= entropy_threshold/2. - # Default: "3.0" per_char_threshold: "3.0" - # Calculate entropy for first N chars of the string. - # Default: "16" truncate: "32" - # Additional functions to ignore while checking unhandled errors. - # Following functions always ignored: - # bytes.Buffer: - # - Write - # - WriteByte - # - WriteRune - # - WriteString - # fmt: - # - Print - # - Printf - # - Println - # - Fprint - # - Fprintf - # - Fprintln - # strings.Builder: - # - Write - # - WriteByte - # - WriteRune - # - WriteString - # io.PipeWriter: - # - CloseWithError - # hash.Hash: - # - Write - # os: - # - Unsetenv - # Default: {} G104: fmt: - Fscanf G111: - # Regexp pattern to find potential directory traversal. - # Default: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" - pattern: "custom\\.Dir\\(\\)" - # Maximum allowed permissions mode for os.Mkdir and os.MkdirAll - # Default: "0750" + pattern: "http\\.Dir\\(\"\\/\"\\)|http\\.Dir\\('\\/'\\)" G301: "0750" - # Maximum allowed permissions mode for os.OpenFile and os.Chmod - # Default: "0600" G302: "0600" - # Maximum allowed permissions mode for os.WriteFile and ioutil.WriteFile - # Default: "0600" G306: "0600" lll: - # Max line length, lines longer will be reported. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option. - # Default: 120. - line-length: 120 - # Tab width in spaces. - # Default: 1 + line-length: 130 tab-width: 1 + staticcheck: + go: "1.15" + # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks + # Default: ["*"] + checks: [ "*", "-SA1019" ] linters: disable-all: true @@ -357,19 +196,15 @@ linters: - gocyclo - ineffassign - goimports -# - nakedret - unparam - unused - prealloc - durationcheck -# - nolintlint - staticcheck - makezero - nilerr - errorlint - bodyclose - - exportloopref -# - gci - gosec - lll fast: false diff --git a/LICENSE b/LICENSE index da2fb54..93caddc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Mikhail Knyazhev +Copyright (c) 2024-2025, Mikhail Knyazhev Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Makefile b/Makefile old mode 100644 new mode 100755 index a0582ad..6c679e3 --- a/Makefile +++ b/Makefile @@ -1,52 +1,50 @@ +SHELL=/bin/bash + .PHONY: install install: - go install github.com/osspkg/devtool@latest - -.PHONY: setup -setup: - devtool setup-lib + go install go.osspkg.com/goppy/v2/cmd/goppy@latest + goppy setup-lib .PHONY: lint lint: - devtool lint + goppy lint .PHONY: license license: - devtool license + goppy license .PHONY: build build: - devtool build --arch=amd64 + goppy build --arch=amd64 .PHONY: tests tests: - devtool test + goppy test -.PHONY: pre-commite -pre-commite: setup lint build tests +.PHONY: pre-commit +pre-commit: install license lint tests build .PHONY: ci -ci: install setup lint build tests - +ci: pre-commit run_example_client_tcp: - ADDRESS="127.0.0.1:8888" NETWORK="tcp" go run -race examples/client/main.go + time ADDRESS="127.0.0.1:8888" NETWORK="tcp" go run -race examples/client/main.go run_example_server_tcp: ADDRESS="127.0.0.1:8888" NETWORK="tcp" go run -race examples/server/main.go run_example_client_udp: - ADDRESS="127.0.0.1:8888" NETWORK="udp" go run -race examples/client/main.go + time ADDRESS="127.0.0.1:8888" NETWORK="udp" go run -race examples/client/main.go run_example_server_udp: ADDRESS="127.0.0.1:8888" NETWORK="udp" go run -race examples/server/main.go run_example_client_unix: - ADDRESS="/tmp/unix.sock" NETWORK="unix" go run -race examples/client/main.go + time ADDRESS="/tmp/unix.sock" NETWORK="unix" go run -race examples/client/main.go run_example_server_unix: ADDRESS="/tmp/unix.sock" NETWORK="unix" go run -race examples/server/main.go run_example_client_quic: - ADDRESS="127.0.0.1:8888" NETWORK="quic" go run -race examples/client/main.go + time ADDRESS="127.0.0.1:8888" NETWORK="quic" go run -race examples/client/main.go run_example_server_quic: ADDRESS="127.0.0.1:8888" NETWORK="quic" go run -race examples/server/main.go diff --git a/address/common.go b/address/common.go index a91562f..15347b6 100644 --- a/address/common.go +++ b/address/common.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -17,48 +17,97 @@ var ( ) func RandomPort(host string) (string, error) { - host = strings.Join([]string{host, "0"}, ":") - addr, err := net.ResolveTCPAddr("tcp", host) + network := "tcp4" + if strings.Contains(host, ":") { + network = "tcp6" + } + + host = net.JoinHostPort(host, "0") + addr, err := net.ResolveTCPAddr(network, host) if err != nil { return host, errors.Wrap(err, ErrResolveTCPAddress) } - l, err := net.ListenTCP("tcp", addr) + + l, err := net.ListenTCP(network, addr) if err != nil { return host, errors.Wrap(err, ErrResolveTCPAddress) } + v := l.Addr().String() + if err = l.Close(); err != nil { return host, errors.Wrap(err, ErrResolveTCPAddress) } + return v, nil } -func CheckHostPort(addr string) string { - hp := strings.Split(addr, ":") - if len(hp) != 2 { - tmp := make([]string, 2) - for i, s := range hp { - if i > 1 { - break +func ResolveIPPort(address string) string { + var ( + host string + port string + ) + + switch true { + case len(address) == 0: + host = "127.0.0.1" + + case IsValidIP(address): + host = address + + case address[0] == '[': + if index := strings.IndexByte(address, ']'); index != -1 { + host = address[1:index] + port = address[index+1:] + if len(port) > 1 && port[0] == ':' { + port = port[1:] } - tmp[i] = s } - hp = tmp + if !IsValidIP(host) { + host = "::1" + } + + case strings.Count(address, ":") > 1: + host = address + if !IsValidIP(host) { + host = "::1" + } + + case strings.Count(address, ":") == 1: + index := strings.IndexByte(address, ':') + host = address[0:index] + port = address[index+1:] + if len(port) > 1 && port[0] == ':' { + port = port[1:] + } + + default: + host = address } - if len(hp[0]) == 0 { - hp[0] = "0.0.0.0" + + if strings.Contains(host, "/") { + return host } - if len(hp[1]) == 0 { - if v, err := RandomPort(hp[0]); err == nil { + + if len(host) == 0 { + host = "0.0.0.0" + } + + if ips, err := net.LookupIP(host); err == nil && len(ips) > 0 { + host = ips[0].String() + } + + if len(port) == 0 || port == ":" { + if v, err := RandomPort(host); err == nil { return v - } else { - hp[1] = "80" } + port = "8080" } - return strings.Join(hp, ":") + + return net.JoinHostPort(host, port) } -func Normalize(defaultPort string, ips ...string) []string { +func FixIPPort(defaultPort string, ips ...string) []string { result := make([]string, 0, len(ips)) for _, ip := range ips { host, port, err := net.SplitHostPort(ip) @@ -66,12 +115,15 @@ func Normalize(defaultPort string, ips ...string) []string { host = ip port = defaultPort } + if !IsValidIP(host) { continue } + if port == "0" { port = defaultPort } + result = append(result, net.JoinHostPort(host, port)) } diff --git a/address/common_test.go b/address/common_test.go index 4b39c30..48decfc 100644 --- a/address/common_test.go +++ b/address/common_test.go @@ -1,18 +1,22 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ package address_test import ( + "fmt" "reflect" + "regexp" "testing" + "go.osspkg.com/casecheck" + "go.osspkg.com/network/address" ) -func TestUnit_Normalize(t *testing.T) { +func TestUnit_FixIPPort(t *testing.T) { tests := []struct { name string port string @@ -28,9 +32,44 @@ func TestUnit_Normalize(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := address.Normalize(tt.port, tt.args...); !reflect.DeepEqual(got, tt.want) { - t.Errorf("Normalize() = %v, want %v", got, tt.want) + if got := address.FixIPPort(tt.port, tt.args...); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FixIPPort() = %v, want %v", got, tt.want) } }) } } + +func TestUnit_ResolveIPPort(t *testing.T) { + tests := []struct { + addr string + want string + }{ + {addr: "", want: `127.0.0.1:[0-9]+`}, + {addr: ":", want: `0.0.0.0:[0-9]+`}, + {addr: ":123", want: "0.0.0.0:123"}, + {addr: "1.1.1.1", want: "1.1.1.1:8080"}, + {addr: "1.1.1.1:", want: "1.1.1.1:8080"}, + {addr: "1.1.1.1:123", want: "1.1.1.1:123"}, + {addr: "0.0.0.0:", want: "0.0.0.0:"}, + {addr: "localhost", want: `(127.0.0.1|\[::1\]):[0-9]+`}, + {addr: "localhost:", want: `(127.0.0.1|\[::1\]):[0-9]+`}, + {addr: "localhost:123", want: `(127.0.0.1|\[::1\]):123`}, + {addr: "a.b.c.d:123", want: "a.b.c.d:123"}, + {addr: "::", want: `\[::\]:[0-9]+`}, + {addr: "[::]", want: `\[::\]:[0-9]+`}, + {addr: "[::]:", want: `\[::\]:[0-9]+`}, + {addr: "[::]:123", want: `\[::\]:123`}, + {addr: "/unix.sock", want: "/unix.sock"}, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("[Case%d]=>'%s'", i, tt.addr), func(t *testing.T) { + got := address.ResolveIPPort(tt.addr) + + t.Logf("from `%s` to `%s`", tt.addr, got) + + ok, err := regexp.Match(tt.want, []byte(got)) + casecheck.NoError(t, err, got) + casecheck.True(t, ok, got) + }) + } +} diff --git a/client/adapter.go b/client/adapter.go new file mode 100644 index 0000000..49cb8c7 --- /dev/null +++ b/client/adapter.go @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package client + +import ( + "io" + "time" + + "go.osspkg.com/network/internal" +) + +type rwc struct { + D internal.Deadline + R io.Reader + W io.Writer + C func() error +} + +func (v *rwc) Read(p []byte) (int, error) { + return v.R.Read(p) +} + +func (v *rwc) Write(p []byte) (int, error) { + return v.W.Write(p) +} + +func (v *rwc) Close() error { + return v.C() +} + +func (v *rwc) SetDeadline(t time.Time) error { + return v.D.SetDeadline(t) +} diff --git a/client/client.go b/client/client.go index 8c2fa7f..3ebbc28 100644 --- a/client/client.go +++ b/client/client.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -11,222 +11,109 @@ import ( "fmt" "io" "net" - "sync" - "time" "github.com/quic-go/quic-go" "go.osspkg.com/algorithms/control" "go.osspkg.com/errors" - "go.osspkg.com/ioutils" + "go.osspkg.com/network/internal" ) -type Client struct { - Network string - Address string - Certificate *Certificate - - Timeout time.Duration - KeepAlive time.Duration - - MaxIdleConns int - BufferSize int - - err error - config *tls.Config - pool *chanPool[*connect] - sem control.Semaphore - once sync.Once -} - -func (v *Client) setup() error { - v.once.Do(func() { - if v.err = internal.IsPassableNetwork(v.Network); v.err != nil { - return - } - - var ( - err error - addr fmt.Stringer - ) - - if err = v.applyTLSCertificate(); err != nil { - v.err = err - return - } - - switch v.Network { - case internal.NetTCP: - addr, err = net.ResolveTCPAddr(v.Network, v.Address) - case internal.NetUDP: - addr, err = net.ResolveUDPAddr(v.Network, v.Address) - case internal.NetQUIC: - if len(v.config.NextProtos) == 0 { - v.config.NextProtos = append(v.config.NextProtos, "quic") - } - addr, err = net.ResolveUDPAddr(internal.NetUDP, v.Address) - case internal.NetUNIX: - addr, err = net.ResolveUnixAddr(v.Network, v.Address) - default: - addr, err = nil, fmt.Errorf("invalid network name, use: tcp, udp, unix, quic") - } - if err != nil { - v.err = err - return - } - - v.BufferSize = internal.NotZero[int](v.BufferSize, 65535) - v.Timeout = internal.NotZeroDuration(v.Timeout, 1*time.Second) - v.KeepAlive = internal.NotZeroDuration(v.KeepAlive, 15*time.Second) - v.MaxIdleConns = internal.NotZero[int](v.MaxIdleConns, 1) - - v.Address = addr.String() - v.sem = control.NewSemaphore(uint64(v.MaxIdleConns)) - - v.pool = newChanPool[*connect](v.MaxIdleConns, func() *connect { - pIdleAt := time.Now().Add(v.KeepAlive) - pconn, pclose, perr := v.dialConnect(context.Background()) - return &connect{ - Conn: pconn, - CloseFunc: pclose, - Err: perr, - IdleAt: pIdleAt, - } - }) - }) - - if v.err != nil { - return v.err +type ( + Client interface { + Call(ctx context.Context, handler func(ctx context.Context, w io.Writer, r io.Reader) error) error } - return nil -} -func (v *Client) applyTLSCertificate() error { - if v.Certificate == nil { - return nil + _client struct { + conf Config + tls *tls.Config + sem control.Semaphore } - if v.config == nil { - v.config = internal.DefaultTLSConfig() +) + +func New(c Config) (Client, error) { + addr, err := c.Resolve() + if err != nil { + return nil, fmt.Errorf("resolve address: %w", err) } - cert, ca, err := parseCertificate(*v.Certificate) + + c.Address = addr.String() + + tlsc, err := c.Certificate.Config(c.Address, c.Network) if err != nil { - return err + return nil, fmt.Errorf("get tls config: %w", err) } - if ca != nil { - v.config.RootCAs = ca + + if c.MaxConns <= 0 { + c.MaxConns = 1 } - if len(cert.Certificate) >= 0 { - v.config.Certificates = append(v.config.Certificates, cert) + + cli := &_client{ + conf: c, + sem: control.NewSemaphore(c.MaxConns), + tls: tlsc, } - v.config.InsecureSkipVerify = v.Certificate.InsecureSkipVerify - return nil + return cli, nil } -func (v *Client) dialConnect(ctx context.Context) (action, func(), error) { - if v.Network == internal.NetQUIC { - conn, err := quic.DialAddr(ctx, v.Address, v.config, &quic.Config{EnableDatagrams: true}) +func (v *_client) conn(ctx context.Context) (internal.Conn, error) { + switch v.conf.Network { + case internal.NetQUIC: + conn, err := quic.DialAddr(ctx, v.conf.Address, v.tls, &quic.Config{EnableDatagrams: false}) if err != nil { - return nil, nil, fmt.Errorf("create connect: %w", err) + return nil, fmt.Errorf("dial quic: %w", err) } - stream, err := conn.OpenStream() + stream, err := conn.OpenStreamSync(ctx) if err != nil { - writeLog(conn.CloseWithError(0, ""), "close connect", v.Network, v.Address) - return nil, nil, fmt.Errorf("open stream: %w", err) + return nil, fmt.Errorf("open stream quic: %w", errors.Wrap(err, conn.CloseWithError(0, ""))) } - - return stream, func() { - writeLog(stream.Close(), "close stream", v.Network, v.Address) - writeLog(conn.CloseWithError(0, ""), "close connect", v.Network, v.Address) - }, nil - } - - if v.Certificate != nil { - dial := &tls.Dialer{ - NetDialer: new(net.Dialer), - Config: v.config, + return &rwc{D: stream, R: stream, W: stream, C: func() error { + return errors.Wrap(stream.Close(), conn.CloseWithError(0, "")) + }}, nil + + case internal.NetTCP: + if v.tls != nil { + dial := &tls.Dialer{ + NetDialer: new(net.Dialer), + Config: v.tls, + } + conn, err := dial.DialContext(ctx, v.conf.Network, v.conf.Address) + if err != nil { + return nil, fmt.Errorf("dial tcp tls: %w", err) + } + return conn, nil } - conn, err := dial.DialContext(ctx, v.Network, v.Address) + fallthrough + + default: + var dial net.Dialer + conn, err := dial.DialContext(ctx, v.conf.Network, v.conf.Address) if err != nil { - return nil, nil, fmt.Errorf("create connect: %w", err) + return nil, fmt.Errorf("dial %s: %w", v.conf.Network, err) } - return conn, func() { - writeLog(conn.Close(), "close connect", v.Network, v.Address) - }, nil - } - - var dial net.Dialer - conn, err := dial.DialContext(ctx, v.Network, v.Address) - if err != nil { - return nil, nil, fmt.Errorf("create connect: %w", err) + return conn, nil } - return conn, func() { - writeLog(conn.Close(), "close connect", v.Network, v.Address) - }, nil } -func (v *Client) Do(in io.Reader, out io.Writer) (err error) { - if err = v.setup(); err != nil { - return - } - +func (v *_client) Call(ctx context.Context, handler func(ctx context.Context, w io.Writer, r io.Reader) error) (e error) { v.sem.Acquire() defer func() { v.sem.Release() }() - conn := v.pool.GetIdleOrCreateConn() - - if err = conn.GetError(); err != nil { - return + conn, err := v.conn(ctx) + if err != nil { + return err } - defer func() { - conn.Err = errors.Wrap(conn.Err, err) - v.pool.PutOrCloseIdleConn(conn) - }() - - errC := make(chan error, 1) - startC := make(chan struct{}) - - go func() { - close(startC) - - if e := internal.Deadline(conn.Conn, v.Timeout*2); e != nil { - errC <- fmt.Errorf("update deadline: %w", e) - return - } - - n, e := ioutils.CopyPack(out, conn.Conn, v.BufferSize) - if e != nil { - errC <- fmt.Errorf("read message: %w", e) - return - } else if n == 0 { - errC <- fmt.Errorf("read message: got 0 bytes") - return - } + stop := internal.DeadlineUpdate(conn) - errC <- nil + defer func() { + stop() + e = errors.Wrap(e, conn.Close()) }() - <-startC - - n, e := ioutils.CopyPack(conn.Conn, in, v.BufferSize) - if e != nil { - err = fmt.Errorf("write message: %w", e) - return - } else if n == 0 { - err = fmt.Errorf("write message: set 0 bytes") - return - } - - if err = <-errC; err != nil { - return - } - - if err = conn.Conn.SetWriteDeadline(time.Now().Add(v.KeepAlive)); err != nil { - err = fmt.Errorf("update deadline: %w", err) - return - } + e = handler(ctx, conn, conn) return } diff --git a/client/config.go b/client/config.go new file mode 100644 index 0000000..f9b58fa --- /dev/null +++ b/client/config.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package client + +import ( + "fmt" + "net" + + "go.osspkg.com/network/internal" +) + +type Config struct { + Network string + Address string + Certificate *Certificate + MaxConns uint64 +} + +func (c Config) Resolve() (addr fmt.Stringer, err error) { + if err := internal.IsPassableNetwork(c.Network); err != nil { + return nil, err + } + + switch c.Network { + case internal.NetTCP: + return net.ResolveTCPAddr(internal.NetTCP, c.Address) + case internal.NetUDP, internal.NetQUIC: + return net.ResolveUDPAddr(internal.NetUDP, c.Address) + case internal.NetUNIX: + return net.ResolveUnixAddr(internal.NetUNIX, c.Address) + default: + return nil, fmt.Errorf("invalid network name, use: tcp, udp, unix, quic") + } +} diff --git a/client/log.go b/client/log.go deleted file mode 100644 index 8598258..0000000 --- a/client/log.go +++ /dev/null @@ -1,10 +0,0 @@ -package client - -import "go.osspkg.com/logx" - -func writeLog(err error, message, network, address string) { - if err == nil { - return - } - logx.Error(message, "err", err, "network", network, "address", address) -} diff --git a/client/pool.go b/client/pool.go deleted file mode 100644 index 2176091..0000000 --- a/client/pool.go +++ /dev/null @@ -1,93 +0,0 @@ -package client - -import ( - "io" - "time" - - "go.osspkg.com/network/internal" -) - -type action interface { - io.Reader - io.Writer - internal.TDeadline -} - -type connect struct { - Conn action - CloseFunc func() - Err error - IdleAt time.Time - Timeout time.Duration -} - -func (c *connect) Close() { - if c.CloseFunc != nil { - c.CloseFunc() - } -} - -func (c *connect) IsFailConn() bool { - return c.Err != nil || time.Now().Add(c.Timeout).After(c.IdleAt) -} - -func (c *connect) GetError() error { - return c.Err -} - -type ( - object interface { - Close() - IsFailConn() bool - GetError() error - } - - chanPool[T object] struct { - c chan T - call func() T - } -) - -func newChanPool[T object](size int, call func() T) *chanPool[T] { - return &chanPool[T]{ - c: make(chan T, size+1), - call: call, - } -} - -func (p *chanPool[T]) GetIdleOrCreateConn() (v T) { - var isNew bool - for { - select { - case v = <-p.c: - default: - v = p.call() - isNew = true - } - - if isNew { - return - } - - if v.IsFailConn() { - v.Close() - continue - } - - return - } -} - -func (p *chanPool[T]) PutOrCloseIdleConn(v T) { - if v.IsFailConn() { - v.Close() - return - } - - select { - case p.c <- v: - return - default: - v.Close() - } -} diff --git a/client/tls.go b/client/tls.go index fee0f29..7c294de 100644 --- a/client/tls.go +++ b/client/tls.go @@ -1,9 +1,17 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + package client import ( "crypto/tls" "crypto/x509" + "net" "os" + + "go.osspkg.com/network/internal" ) type Certificate struct { @@ -13,7 +21,7 @@ type Certificate struct { InsecureSkipVerify bool `yaml:"insecure_skip_verify"` } -func parseCertificate(c Certificate) (cert tls.Certificate, ca *x509.CertPool, err error) { +func (c *Certificate) parse() (cert tls.Certificate, ca *x509.CertPool, err error) { if len(c.CertFile) > 0 || len(c.KeyFile) > 0 { cert, err = tls.LoadX509KeyPair(c.CertFile, c.KeyFile) if err != nil { @@ -28,3 +36,42 @@ func parseCertificate(c Certificate) (cert tls.Certificate, ca *x509.CertPool, e } return } + +func (c *Certificate) Config(address, network string) (*tls.Config, error) { + if c == nil { + return nil, nil + } + + switch network { + case internal.NetUNIX, internal.NetUDP: + return nil, nil + } + + conf := internal.DefaultTLSConfig() + + cert, ca, err := c.parse() + if err != nil { + return nil, err + } + if ca != nil { + conf.RootCAs = ca + } + if len(cert.Certificate) >= 0 { + conf.Certificates = append(conf.Certificates, cert) + } + + host, _, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + + conf.ServerName = host + conf.InsecureSkipVerify = c.InsecureSkipVerify + + switch network { + case internal.NetQUIC: + conf.NextProtos = append(conf.NextProtos, "quic") + } + + return conf, nil +} diff --git a/epoll/common.go b/epoll/common.go index b65e4fc..6d496b0 100644 --- a/epoll/common.go +++ b/epoll/common.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/epoll/epoll.go b/epoll/epoll.go index 14eaa63..711213a 100644 --- a/epoll/epoll.go +++ b/epoll/epoll.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -16,8 +16,9 @@ import ( "go.osspkg.com/errors" "go.osspkg.com/ioutils" "go.osspkg.com/logx" - netfd "go.osspkg.com/network/fd" "golang.org/x/sys/unix" + + netfd "go.osspkg.com/network/fd" ) type ( diff --git a/epoll/epoll_net.go b/epoll/epoll_net.go index 8617192..e48f9d7 100644 --- a/epoll/epoll_net.go +++ b/epoll/epoll_net.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/epoll/options.go b/epoll/options.go index 2bd12d6..1e71602 100644 --- a/epoll/options.go +++ b/epoll/options.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/epoll/server_tcp.go b/epoll/server_tcp.go index 900c77a..eaac02f 100644 --- a/epoll/server_tcp.go +++ b/epoll/server_tcp.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -14,9 +14,10 @@ import ( "go.osspkg.com/errors" "go.osspkg.com/logx" - "go.osspkg.com/network/address" "go.osspkg.com/syncing" "go.osspkg.com/xc" + + "go.osspkg.com/network/address" ) type ( @@ -44,7 +45,7 @@ func (s *ServerTCP) init() (err error) { return fmt.Errorf("epoll tcp: handler is empty") } s.wg = syncing.NewGroup() - s.Config.Addr = address.CheckHostPort(s.Config.Addr) + s.Config.Addr = address.ResolveIPPort(s.Config.Addr) if s.Config.CountEvents == 0 { s.Config.CountEvents = 100 } diff --git a/errs/errs.go b/errs/errs.go new file mode 100644 index 0000000..1626570 --- /dev/null +++ b/errs/errs.go @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package errs + +import ( + "io" + "net/http" + "strings" + + "github.com/quic-go/quic-go" + "go.osspkg.com/errors" +) + +func IsClosed(err error) bool { + if err == nil { + return false + } + if errors.Is(err, io.EOF) || + errors.Is(err, quic.ErrServerClosed) || + errors.Is(err, http.ErrServerClosed) || + strings.Contains(err.Error(), "i/o timeout") || + strings.Contains(err.Error(), "use of closed network connection") || + strings.Contains(err.Error(), "deadline exceeded") || + strings.Contains(err.Error(), "server closed") { + return true + } + return false +} diff --git a/examples/client/main.go b/examples/client/main.go index da670f8..3243818 100644 --- a/examples/client/main.go +++ b/examples/client/main.go @@ -1,31 +1,38 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ package main import ( - "bytes" + "context" "fmt" "io" "os" "sync/atomic" "time" - "go.osspkg.com/network/client" + "go.osspkg.com/ioutils/data" "go.osspkg.com/syncing" + + "go.osspkg.com/network/client" ) func main() { - cli := &client.Client{ - Address: os.Getenv("ADDRESS"), - Network: os.Getenv("NETWORK"), - MaxIdleConns: 10, + config := client.Config{ + Address: os.Getenv("ADDRESS"), + Network: os.Getenv("NETWORK"), + MaxConns: 10, } - if cli.Network == "quic" { - cli.Certificate = &client.Certificate{InsecureSkipVerify: true} + if config.Network == "quic" { + config.Certificate = &client.Certificate{InsecureSkipVerify: true} + } + + cli, err := client.New(config) + if err != nil { + panic(err) } var ( @@ -36,19 +43,29 @@ func main() { for i := 0; i < 3; i++ { fmt.Println("------------ STEP", i, "---------------") wg := syncing.NewGroup() - for i := 0; i < 100000; i++ { + for i := 0; i < 10000; i++ { i := i wg.Background(func() { - in := bytes.NewBufferString(fmt.Sprintf("<- %d ->", i)) - out := bytes.NewBuffer(nil) - if err := cli.Do(in, out); err != nil { - fmt.Println(i, "ERR", err) + buff := data.NewBuffer(1024) + buff.WriteString(fmt.Sprintf("<- %d ->", i)) + err := cli.Call(context.TODO(), func(ctx context.Context, w io.Writer, r io.Reader) error { + if _, err := buff.WriteTo(w); err != nil { + return err + } + buff.Reset() + if _, err := buff.ReadFrom(r); err != nil { + return err + } + return nil + }) + if err != nil { + fmt.Println(i, "E", err) atomic.AddInt64(&fail, 1) - return + } else { + fmt.Println(i, "B", buff.String()) + atomic.AddInt64(&good, 1) } - b, err := io.ReadAll(out) - fmt.Println(i, "E", err, "B", string(b)) - atomic.AddInt64(&good, 1) + }) } wg.Wait() diff --git a/examples/epoll-server/main.go b/examples/epoll-server/main.go index ffa5987..e17d44c 100644 --- a/examples/epoll-server/main.go +++ b/examples/epoll-server/main.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -9,8 +9,9 @@ import ( "context" "io" - "go.osspkg.com/network/epoll" "go.osspkg.com/xc" + + "go.osspkg.com/network/epoll" ) func main() { diff --git a/examples/server/main.go b/examples/server/main.go index e588609..62567fa 100644 --- a/examples/server/main.go +++ b/examples/server/main.go @@ -1,48 +1,51 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ package main import ( + "context" "fmt" "io" + "net" "os" + "go.osspkg.com/ioutils/data" "go.osspkg.com/logx" + "go.osspkg.com/network/listen" "go.osspkg.com/network/server" - "go.osspkg.com/xc" ) func main() { logx.SetLevel(logx.LevelDebug) - config := &server.Config{ + config := server.Config{ Address: os.Getenv("ADDRESS"), Network: os.Getenv("NETWORK"), } if config.Network == "quic" { - config.Certs = append(config.Certs, listen.Certificate{AutoGenerate: true, Addresses: []string{"127.0.0.1"}}) + config.SSL = &server.SSL{ + Certs: []listen.Certificate{ + {AutoGenerate: true, Addresses: []string{"127.0.0.1"}}, + }, + } } - srv := server.New(*config) + srv := server.New(config) - echo := &Echo{} - srv.HandleFunc(echo) + srv.HandleFunc(func(ctx context.Context, w io.Writer, r io.Reader, addr net.Addr) { + buff := data.NewBuffer(1024) + _, err := buff.ReadFrom(r) + fmt.Println("[------", addr.String(), "------]", err, buff.String()) + buff.Seek(0, 0) + buff.WriteTo(w) + }) - if err := srv.ListenAndServe(xc.New()); err != nil { + if err := srv.ListenAndServe(context.TODO()); err != nil { panic(err) } } - -type Echo struct { -} - -func (*Echo) Handler(ctx server.Ctx) { - b, err := io.ReadAll(ctx) - fmt.Println("[------", ctx.Addr(), "------]", err, string(b)) - ctx.Write(b) -} diff --git a/fd/fd.go b/fd/fd.go index 64c8b8a..619a692 100644 --- a/fd/fd.go +++ b/fd/fd.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ diff --git a/go.mod b/go.mod index 6a1ba4a..293b7f7 100644 --- a/go.mod +++ b/go.mod @@ -1,30 +1,31 @@ module go.osspkg.com/network -go 1.22.10 +go 1.23.6 require ( - github.com/quic-go/quic-go v0.48.2 - go.osspkg.com/algorithms v1.4.1 + github.com/quic-go/quic-go v0.50.1 + go.osspkg.com/algorithms v1.5.0 + go.osspkg.com/casecheck v0.3.0 go.osspkg.com/do v0.1.3 go.osspkg.com/errors v0.3.1 - go.osspkg.com/ioutils v0.4.8 + go.osspkg.com/ioutils v0.5.1 go.osspkg.com/logx v0.4.2 - go.osspkg.com/syncing v0.3.0 + go.osspkg.com/syncing v0.3.1 go.osspkg.com/xc v0.4.0 - golang.org/x/sys v0.29.0 + golang.org/x/sys v0.31.0 ) require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/onsi/ginkgo/v2 v2.22.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/onsi/ginkgo/v2 v2.22.2 // indirect go.uber.org/mock v0.5.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/tools v0.28.0 // indirect + golang.org/x/tools v0.29.0 // indirect ) diff --git a/go.sum b/go.sum index 82d41b9..bc4233a 100644 --- a/go.sum +++ b/go.sum @@ -10,59 +10,57 @@ github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/Z github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= -github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= -github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= +github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q= +github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.osspkg.com/algorithms v1.4.1 h1:yCXiNHu+MGiUYU7wFsCg2Q0drLPnu1q13cIrPAHDlXU= -go.osspkg.com/algorithms v1.4.1/go.mod h1:JarWCfCmgxBkk2tA/ABeHScK9WN8azu+bEc27+dF/Gw= +go.osspkg.com/algorithms v1.5.0 h1:JGmZh+keG0c019K174KggmxeKQ8HzkqnZzYebjS/xi0= +go.osspkg.com/algorithms v1.5.0/go.mod h1:zgjWqmR5KQG/mJSrGLsPug75CzHRZHXj2Ri6UZv4xV8= go.osspkg.com/casecheck v0.3.0 h1:x15blEszElbrHrEH5H02JIIhGIg/lGZzIt1kQlD3pwM= go.osspkg.com/casecheck v0.3.0/go.mod h1:TRFXDMFJEOtnlp3ET2Hix3osbxwPWhvaiT/HfD3+gBA= go.osspkg.com/do v0.1.3 h1:oNXeWZOQUv73LQLawKxfAyN1wacTtRZQVlrkN8nj+sw= go.osspkg.com/do v0.1.3/go.mod h1:hOQEum85f8Kc4m8PWUAECDQ/mTtQ4362ABLD+KW5/vk= go.osspkg.com/errors v0.3.1 h1:F9m/EEd/Ot2jba/TV7tvVRIpWXzIpNLc7vRJKcBD86A= go.osspkg.com/errors v0.3.1/go.mod h1:dKXe6Rt07nzY7OyKQNZ8HGBicZ2uQ5TKEoVFnVFOK44= -go.osspkg.com/ioutils v0.4.8 h1:7o7n6eypWdu3EF8i/ocnuyqDtYXJUqds+Chd1XmZp5s= -go.osspkg.com/ioutils v0.4.8/go.mod h1:58HhG2NHf9JUtixAH3R2XISlUmJruwVIUZ3039QVjOY= +go.osspkg.com/ioutils v0.5.0 h1:oCNOJS5AN8pS9hzgi0gOGQRpUp9R6D+UW+5eDpqSdqg= +go.osspkg.com/ioutils v0.5.0/go.mod h1:58HhG2NHf9JUtixAH3R2XISlUmJruwVIUZ3039QVjOY= +go.osspkg.com/ioutils v0.5.1 h1:qzoOECBxChZUxmp6p72XvQRHjDFsVw1kJ2oaFZycNv8= +go.osspkg.com/ioutils v0.5.1/go.mod h1:XRASOo5GKzVaJMCXXZQ4//ymPWq90iyKm1qTQcBEsyo= go.osspkg.com/logx v0.4.2 h1:3kqG7EaaT/DxpHytQm4MfcrmDhYf8ha9/iRpVjpRt88= go.osspkg.com/logx v0.4.2/go.mod h1:mGbH9hdkeC0h9Gw1uWgQfi9MmlANcqNLffB0wxIDpsQ= -go.osspkg.com/syncing v0.3.0 h1:yBkCsDPEt12a+qagInFFt7+ZongfT+GjSQl7nBmcybI= -go.osspkg.com/syncing v0.3.0/go.mod h1:Dpe0ljlEG6cI2Y9PxEjKiYEX2sgs1eUjWNVjFu4/iB0= -go.osspkg.com/xc v0.3.1 h1:6De75eXdP9CVXqgQOcCWLPyAqFw9zP5lM6rV9MLGiCE= -go.osspkg.com/xc v0.3.1/go.mod h1:6dUG4Y/Q2NMhc5vYrNy0ehWIaHQtAi+MFfc22onQHEs= +go.osspkg.com/syncing v0.3.1 h1:zt5o/X5DQ/GE5OQTKkq1nNWJMg7EcYhw0YiwMGuA0f8= +go.osspkg.com/syncing v0.3.1/go.mod h1:Dpe0ljlEG6cI2Y9PxEjKiYEX2sgs1eUjWNVjFu4/iB0= go.osspkg.com/xc v0.4.0 h1:MGntRGa3EPCpfrTbWEN7x475BAsAtRYGpYEYJ5mE0I8= go.osspkg.com/xc v0.4.0/go.mod h1:HWDrUQOKMkQser1teXqnFNMB1WVD0YsyIuM1vIKny7U= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU= -golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= -golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= +golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= +google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/datapool.go b/internal/datapool.go new file mode 100644 index 0000000..ec815a3 --- /dev/null +++ b/internal/datapool.go @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package internal + +import ( + "go.osspkg.com/ioutils/data" + "go.osspkg.com/ioutils/pool" +) + +var DataPool = pool.New[*data.Buffer](func() *data.Buffer { + return data.NewBuffer(512) +}) diff --git a/internal/deadline.go b/internal/deadline.go new file mode 100644 index 0000000..1015057 --- /dev/null +++ b/internal/deadline.go @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package internal + +import ( + "io" + "time" +) + +type Conn interface { + io.ReadWriteCloser + Deadline +} + +type Deadline interface { + SetDeadline(t time.Time) error +} + +func DeadlineUpdate(conn Deadline) func() { + tik := time.NewTicker(time.Second * 5) + closeC := make(chan struct{}) + + go func() { + for { + select { + case <-closeC: + return + case v := <-tik.C: + if err := conn.SetDeadline(v.Add(time.Second * 10)); err != nil { + return + } + } + } + }() + + return tik.Stop +} diff --git a/internal/errors.go b/internal/errors.go index 67a63d4..da7dbb6 100644 --- a/internal/errors.go +++ b/internal/errors.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -20,13 +20,6 @@ var ( ErrServAlreadyRunning = errors.New("server already running") ) -func NormalCloseError(err error) error { - if IsNormalCloseError(err) { - return nil - } - return err -} - func IsNormalCloseError(err error) bool { if err == nil || errors.Is(err, io.EOF) || @@ -41,9 +34,13 @@ func IsNormalCloseError(err error) bool { return false } -func WriteErrLog(message string, err error, addr net.Addr) { +func Log(message string, err error, addr net.Addr) { if err == nil || IsNormalCloseError(err) { return } - logx.Warn(message, "err", err, "addr", addr) + if addr == nil { + logx.Warn(message, "err", err) + } else { + logx.Warn(message, "err", err, "addr", addr) + } } diff --git a/internal/network.go b/internal/network.go index 33078df..10f2226 100644 --- a/internal/network.go +++ b/internal/network.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -7,9 +7,6 @@ package internal import ( "fmt" - "time" - - "go.osspkg.com/errors" ) const ( @@ -27,16 +24,3 @@ func IsPassableNetwork(network string) error { return fmt.Errorf("invalid network type, use: tcp, udp, unix, quic") } } - -type TDeadline interface { - SetReadDeadline(t time.Time) error - SetWriteDeadline(t time.Time) error -} - -func Deadline(c TDeadline, ttl time.Duration) error { - t := time.Now().Add(ttl) - return errors.Wrap( - c.SetReadDeadline(t), - c.SetWriteDeadline(t), - ) -} diff --git a/internal/packet.go b/internal/packet.go new file mode 100644 index 0000000..94e510a --- /dev/null +++ b/internal/packet.go @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package internal + +import ( + "net" +) + +const ( + UDPPacketSize = 65535 +) + +type PacketWrite struct { + Addr net.Addr + Conn interface { + WriteTo(p []byte, addr net.Addr) (n int, err error) + } +} + +func (a *PacketWrite) Write(p []byte) (n int, err error) { + from, count := 0, len(p) + + defer func() { + n = from + }() + + for i := 0; i < count; i += UDPPacketSize { + to := min(count, from+UDPPacketSize) + if n, err = a.Conn.WriteTo(p[from:to], a.Addr); err != nil { + break + } + from += n + } + + return +} diff --git a/internal/packet_test.go b/internal/packet_test.go new file mode 100644 index 0000000..2308e3a --- /dev/null +++ b/internal/packet_test.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package internal_test + +import ( + "net" + "testing" + + "go.osspkg.com/casecheck" + "go.osspkg.com/ioutils/data" + + "go.osspkg.com/network/internal" +) + +type mockConn struct { + B *data.Buffer +} + +func (m *mockConn) WriteTo(p []byte, _ net.Addr) (n int, err error) { + return m.B.Write(p) +} + +func TestUnit_PacketWrite(t *testing.T) { + a := internal.PacketWrite{ + Addr: nil, + Conn: &mockConn{ + B: data.NewBuffer(0), + }, + } + + n, err := a.Write(make([]byte, 100_000)) + casecheck.NoError(t, err) + casecheck.Equal(t, 100_000, n) +} diff --git a/internal/times.go b/internal/times.go deleted file mode 100644 index 556fafd..0000000 --- a/internal/times.go +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package internal - -import "time" - -type Comparable interface { - ~int | ~int8 | ~int16 | ~int32 | ~int64 | - ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | - ~float32 | ~float64 -} - -func NotZeroDuration(args ...time.Duration) time.Duration { - for _, arg := range args { - if arg > 0 { - return arg - } - } - return 0 -} - -func NotZero[T Comparable](args ...T) T { - for _, arg := range args { - if arg > 0 { - return arg - } - } - return 0 -} diff --git a/internal/tls.go b/internal/tls.go index 19ede7b..57743f4 100644 --- a/internal/tls.go +++ b/internal/tls.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + package internal import ( @@ -13,12 +18,12 @@ func DefaultTLSConfig() *tls.Config { tls.CurveP521, tls.CurveP384, tls.CurveP256, + tls.X25519, }, CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, }, } } diff --git a/listen/listener.go b/listen/listener.go index f24f8b7..99026a5 100644 --- a/listen/listener.go +++ b/listen/listener.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -17,24 +17,16 @@ import ( "go.osspkg.com/network/internal" ) -func New(ctx context.Context, network, address string, certs ...Certificate) (io.Closer, error) { - switch network { - case internal.NetUDP, internal.NetUNIX: - if len(certs) > 0 { - return nil, fmt.Errorf("%s not support tls", network) - } - default: - } - +func New(ctx context.Context, network, address string, ssl *SSL) (io.Closer, error) { switch network { case internal.NetTCP: - return newListen(ctx, network, address, certs...) + return newListen(ctx, network, address, ssl) case internal.NetUDP: return newListenPacket(ctx, network, address) case internal.NetUNIX: - return newListen(ctx, network, address) + return newListen(ctx, network, address, nil) case internal.NetQUIC: - return newListenQUIC(ctx, address, certs...) + return newListenQUIC(ctx, address, ssl) default: return nil, fmt.Errorf("invalid network type, use: tcp, udp, unix") } @@ -45,32 +37,36 @@ func newListenPacket(ctx context.Context, network, address string) (net.PacketCo return lc.ListenPacket(ctx, network, address) } -func newListen(ctx context.Context, network, address string, certs ...Certificate) (l net.Listener, err error) { +func newListen(ctx context.Context, network, address string, ssl *SSL) (l net.Listener, err error) { var lc net.ListenConfig if l, err = lc.Listen(ctx, network, address); err != nil { return nil, err } - if len(certs) == 0 { + if ssl == nil || len(ssl.Certs) == 0 { return } var conf *tls.Config - if conf, err = NewTLSConfig(certs...); err != nil { + if conf, err = NewTLSConfig(ssl); err != nil { return nil, err } return tls.NewListener(l, conf), nil } -func newListenQUIC(_ context.Context, address string, certs ...Certificate) (l *quic.Listener, err error) { - if len(certs) == 0 { +func newListenQUIC(_ context.Context, address string, ssl *SSL) (l *quic.Listener, err error) { + if ssl == nil || len(ssl.Certs) == 0 { return nil, fmt.Errorf("QUIC cant work without tls") } + + if len(ssl.NextProtos) == 0 { + ssl.NextProtos = append(ssl.NextProtos, "quic") + } + var conf *tls.Config - if conf, err = NewTLSConfig(certs...); err != nil { + if conf, err = NewTLSConfig(ssl); err != nil { return nil, err } - conf.NextProtos = append(conf.NextProtos, "quic") return quic.ListenAddr(address, conf, &quic.Config{EnableDatagrams: true}) } diff --git a/listen/tls.go b/listen/tls.go index 0eaf6b3..3509a8d 100644 --- a/listen/tls.go +++ b/listen/tls.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + package listen import ( @@ -10,10 +15,16 @@ import ( "math/big" "net" "os" + "time" "go.osspkg.com/network/internal" ) +type SSL struct { + Certs []Certificate + NextProtos []string +} + type Certificate struct { CAFile string `yaml:"ca_file"` CertFile string `yaml:"cert_file"` @@ -22,15 +33,15 @@ type Certificate struct { AutoGenerate bool `yaml:"auto_generate"` } -func NewTLSConfig(certs ...Certificate) (*tls.Config, error) { +func NewTLSConfig(ssl *SSL) (*tls.Config, error) { rootCA := x509.NewCertPool() - certificates := make([]tls.Certificate, 0, len(certs)) + certificates := make([]tls.Certificate, 0, len(ssl.Certs)) var ( c tls.Certificate err error ) - for _, cert := range certs { + for _, cert := range ssl.Certs { if cert.AutoGenerate { c, err = generateCertificate(cert.Addresses) } else { @@ -45,6 +56,7 @@ func NewTLSConfig(certs ...Certificate) (*tls.Config, error) { config := internal.DefaultTLSConfig() config.Certificates = certificates config.RootCAs = rootCA + config.NextProtos = append(config.NextProtos, ssl.NextProtos...) return config, nil } @@ -78,24 +90,33 @@ func dnsNames(addresses []string) (ips []net.IP, domains []string) { ips = append(ips, net.ParseIP(san)) continue } - domains = append(domains, "localhost", address) + domains = append(domains, address) } return } func generateCertificate(address []string) (tls.Certificate, error) { - key, err := rsa.GenerateKey(rand.Reader, 1024) + key, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return tls.Certificate{}, err } + commonName := "*" ips, domains := dnsNames(address) + if len(domains) > 0 { + commonName = domains[0] + } template := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{CommonName: "*"}, - IPAddresses: ips, - DNSNames: domains, + SerialNumber: big.NewInt(1), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + Subject: pkix.Name{CommonName: commonName}, + IPAddresses: ips, + DNSNames: domains, + PermittedDNSDomainsCritical: true, + NotBefore: time.Now().UTC(), + NotAfter: time.Now().Add(time.Hour * 24 * 365 * 2).UTC(), } certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) diff --git a/server/common.go b/server/common.go deleted file mode 100644 index a0a6a15..0000000 --- a/server/common.go +++ /dev/null @@ -1,19 +0,0 @@ -package server - -import ( - "context" - "io" -) - -type ( - Ctx interface { - io.Reader - io.Writer - Addr() string - Context() context.Context - } - - Handler interface { - Handler(ctx Ctx) - } -) diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..1a020b1 --- /dev/null +++ b/server/config.go @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. + * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. + */ + +package server + +import "go.osspkg.com/network/listen" + +type ( + Config struct { + Address string `yaml:"address"` + Network string `yaml:"network"` + SSL *SSL `yaml:"ssl,omitempty"` + } + SSL struct { + Certs []listen.Certificate `yaml:"certs,omitempty"` + NextProtos []string `yaml:"next_protos,omitempty"` + } +) diff --git a/server/prc.go b/server/prc.go deleted file mode 100644 index c288a05..0000000 --- a/server/prc.go +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package server - -import ( - "bytes" - "context" - "net" - - "go.osspkg.com/ioutils/pool" -) - -var ( - poolPRC = pool.New[*prc](newPRC) -) - -type ( - tPRConn interface { - WriteTo(p []byte, addr net.Addr) (n int, err error) - } - - prc struct { - conn tPRConn - rb *bytes.Buffer - wb *bytes.Buffer - ctx context.Context - addr net.Addr - } -) - -func newPRC() *prc { - return &prc{ - rb: bytes.NewBuffer(make([]byte, 0, 512)), - wb: bytes.NewBuffer(make([]byte, 0, 512)), - } -} - -func (v *prc) Setup(ctx context.Context, conn tPRConn, addr net.Addr) { - v.ctx = ctx - v.conn = conn - v.addr = addr -} - -func (v *prc) Reset() { - v.conn = nil - v.rb.Reset() - v.wb.Reset() - v.ctx = nil - v.addr = nil -} - -func (v *prc) Pickup(b []byte) (int, error) { - return v.rb.Write(b) -} - -func (v *prc) Release() (int, error) { - return v.conn.WriteTo(v.wb.Bytes(), v.addr) -} - -func (v *prc) Read(b []byte) (int, error) { - return v.rb.Read(b) -} - -func (v *prc) Write(b []byte) (int, error) { - return v.wb.Write(b) -} - -func (v *prc) Addr() string { - return v.addr.String() -} - -func (v *prc) Context() context.Context { - return v.ctx -} diff --git a/server/rwc.go b/server/rwc.go deleted file mode 100644 index 4315a07..0000000 --- a/server/rwc.go +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. - * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. - */ - -package server - -import ( - "bytes" - "context" - "io" - "net" - - "go.osspkg.com/ioutils" - "go.osspkg.com/ioutils/pool" -) - -var ( - poolRWC = pool.New[*rwc](newRWC) -) - -type ( - tRWConn interface { - io.ReadWriter - } - - rwc struct { - conn tRWConn - rb *bytes.Buffer - wb *bytes.Buffer - bsize int - ctx context.Context - addr net.Addr - } -) - -func newRWC() *rwc { - return &rwc{ - rb: bytes.NewBuffer(make([]byte, 0, 512)), - wb: bytes.NewBuffer(make([]byte, 0, 512)), - } -} - -func (v *rwc) Setup(ctx context.Context, bsize int, conn tRWConn, addr net.Addr) { - v.ctx = ctx - v.conn = conn - v.addr = addr - v.bsize = bsize -} - -func (v *rwc) Reset() { - v.conn = nil - v.rb.Reset() - v.wb.Reset() - v.bsize = 0 - v.ctx = nil - v.addr = nil -} - -func (v *rwc) Pickup() error { - n, err := ioutils.CopyPack(v.rb, v.conn, v.bsize) - if err != nil { - return err - } - if n == 0 { - return io.EOF - } - return nil -} - -func (v *rwc) Release() error { - n, err := ioutils.CopyPack(v.conn, v.wb, v.bsize) - if err != nil { - return err - } - if n == 0 { - return io.EOF - } - return nil -} - -func (v *rwc) Read(b []byte) (int, error) { - return v.rb.Read(b) -} - -func (v *rwc) Write(b []byte) (int, error) { - return v.wb.Write(b) -} - -func (v *rwc) Addr() string { - return v.addr.String() -} - -func (v *rwc) Context() context.Context { - return v.ctx -} diff --git a/server/server.go b/server/server.go index fd19d73..4cfbcbd 100644 --- a/server/server.go +++ b/server/server.go @@ -1,5 +1,5 @@ /* - * Copyright (c) 2024 Mikhail Knyazhev . All rights reserved. + * Copyright (c) 2024-2025 Mikhail Knyazhev . All rights reserved. * Use of this source code is governed by a BSD 3-Clause license that can be found in the LICENSE file. */ @@ -12,43 +12,33 @@ import ( "io" "net" "os" - "time" "github.com/quic-go/quic-go" "go.osspkg.com/errors" "go.osspkg.com/ioutils/fs" + "go.osspkg.com/syncing" + "go.osspkg.com/network/address" "go.osspkg.com/network/internal" "go.osspkg.com/network/listen" - "go.osspkg.com/syncing" - "go.osspkg.com/xc" ) type ( - TServer interface { - HandleFunc(h Handler) - ListenAndServe(ctx xc.Context) error - } - - Config struct { - Address string `yaml:"address"` - Network string `yaml:"network"` - Certs []listen.Certificate `yaml:"certs,omitempty"` - Timeout time.Duration `yaml:"timeout,omitempty"` - KeepAlive time.Duration `yaml:"keep_alive,omitempty"` - BufferSize int `yaml:"buffer_size,omitempty"` + Server interface { + HandleFunc(func(ctx context.Context, w io.Writer, r io.Reader, addr net.Addr)) + ListenAndServe(ctx context.Context) error } _server struct { - conf Config - listener io.Closer - handler Handler - sync syncing.Switch - wg syncing.Group + conf Config + listener io.Closer + handlerFunc func(ctx context.Context, w io.Writer, r io.Reader, addr net.Addr) + sync syncing.Switch + wg syncing.Group } ) -func New(conf Config) TServer { +func New(conf Config) Server { return &_server{ conf: conf, sync: syncing.NewSwitch(), @@ -56,37 +46,33 @@ func New(conf Config) TServer { } } -func (v *_server) HandleFunc(h Handler) { +func (v *_server) HandleFunc(fn func(context.Context, io.Writer, io.Reader, net.Addr)) { if v.sync.IsOn() { return } - v.handler = h + v.handlerFunc = fn } -func (v *_server) ListenAndServe(ctx xc.Context) error { - defer func() { - ctx.Close() - }() - - if v.handler == nil { +func (v *_server) ListenAndServe(ctx context.Context) error { + if v.handlerFunc == nil { return fmt.Errorf("handler not found") } if !v.sync.On() { return internal.ErrServAlreadyRunning } - if err := v.build(ctx.Context()); err != nil { + if err := v.build(ctx); err != nil { return err } if l, ok := v.listener.(*quic.Listener); ok { - return v.handlingQUIC(ctx.Context(), l) + return v.handlingQUIC(ctx, l) } if l, ok := v.listener.(net.Listener); ok { - return v.handlingConn(ctx.Context(), l) + return v.handlingConn(ctx, l) } if l, ok := v.listener.(net.PacketConn); ok { - return v.handlingPacketConn(ctx.Context(), l) + return v.handlingPacketConn(ctx, l) } return fmt.Errorf("unknown listener") @@ -102,11 +88,11 @@ func (v *_server) close() { func (v *_server) build(ctx context.Context) error { switch v.conf.Network { case internal.NetTCP: - v.conf.Address = address.CheckHostPort(v.conf.Address) + v.conf.Address = address.ResolveIPPort(v.conf.Address) case internal.NetUDP: - v.conf.Address = address.CheckHostPort(v.conf.Address) + v.conf.Address = address.ResolveIPPort(v.conf.Address) case internal.NetQUIC: - v.conf.Address = address.CheckHostPort(v.conf.Address) + v.conf.Address = address.ResolveIPPort(v.conf.Address) case internal.NetUNIX: if fs.FileExist(v.conf.Address) { if err := os.Remove(v.conf.Address); err != nil { @@ -115,11 +101,13 @@ func (v *_server) build(ctx context.Context) error { } } - v.conf.Timeout = internal.NotZeroDuration(v.conf.Timeout, 1*time.Second) - v.conf.KeepAlive = internal.NotZeroDuration(v.conf.KeepAlive, 15*time.Second) - v.conf.BufferSize = internal.NotZero[int](v.conf.BufferSize, 65535) + ssl := &listen.SSL{} + if v.conf.SSL != nil { + ssl.Certs = append(ssl.Certs, v.conf.SSL.Certs...) + ssl.NextProtos = append(ssl.NextProtos, v.conf.SSL.NextProtos...) + } - l, err := listen.New(ctx, v.conf.Network, v.conf.Address, v.conf.Certs...) + l, err := listen.New(ctx, v.conf.Network, v.conf.Address, ssl) if err != nil { return err } @@ -130,7 +118,11 @@ func (v *_server) build(ctx context.Context) error { func (v *_server) handlingPacketConn(ctx context.Context, l net.PacketConn) error { ctx, cancel := context.WithCancel(ctx) + + stop := internal.DeadlineUpdate(l) + defer func() { + stop() cancel() v.wg.Wait() }() @@ -140,7 +132,7 @@ func (v *_server) handlingPacketConn(ctx context.Context, l net.PacketConn) erro v.close() }) - buff := make([]byte, v.conf.BufferSize) + buff := make([]byte, internal.UDPPacketSize) for { select { @@ -151,46 +143,39 @@ func (v *_server) handlingPacketConn(ctx context.Context, l net.PacketConn) erro n, addr, err := l.ReadFrom(buff) if err != nil { - internal.WriteErrLog("PacketConn: read message", err, addr) + internal.Log("PacketConn: read message", err, addr) return err } - if n == 0 { - internal.WriteErrLog("PacketConn: read message", fmt.Errorf("empty request"), addr) - continue - } - cp := poolPRC.Get() - cp.Setup(ctx, l, addr) + req := internal.DataPool.Get() - if _, err = cp.Pickup(buff[:n]); err != nil { - poolPRC.Put(cp) - internal.WriteErrLog("PacketConn: read message", err, addr) - continue + if _, err = req.Write(buff[:n]); err != nil { + internal.Log("PacketConn: read message", err, addr) + return err } v.wg.Background(func() { defer func() { if e := recover(); e != nil { - internal.WriteErrLog("PacketConn: panic", fmt.Errorf("%+v", e), addr) + internal.Log("PacketConn: panic", fmt.Errorf("%+v", e), addr) } - defer poolPRC.Put(cp) - }() - v.handler.Handler(cp) + internal.DataPool.Put(req) + }() - if _, e := cp.Release(); e != nil { - internal.WriteErrLog("PacketConn: write message", e, addr) - } + v.handlerFunc(ctx, &internal.PacketWrite{Addr: addr, Conn: l}, req, addr) }) } } func (v *_server) handlingConn(ctx context.Context, l net.Listener) error { ctx, cancel := context.WithCancel(ctx) + v.wg.Background(func() { <-ctx.Done() v.close() }) + defer func() { cancel() v.wg.Wait() @@ -205,7 +190,7 @@ func (v *_server) handlingConn(ctx context.Context, l net.Listener) error { conn, err := l.Accept() if err != nil { - cancel() + internal.Log("Conn: accept", err, nil) return err } @@ -213,60 +198,38 @@ func (v *_server) handlingConn(ctx context.Context, l net.Listener) error { if tc, ok := conn.(*tls.Conn); ok { if err = tc.HandshakeContext(ctx); err != nil { - internal.WriteErrLog("Conn: handshake", err, addr) - conn.Close() //nolint: errcheck + internal.Log("Conn: handshake", err, addr) + internal.Log("Conn: close", conn.Close(), addr) continue } } v.wg.Background(func() { - bgCtx, bgCancel := context.WithCancel(ctx) + stop := internal.DeadlineUpdate(conn) + defer func() { if e := recover(); e != nil { - internal.WriteErrLog("Conn: panic", fmt.Errorf("%+v", e), addr) - } - conn.Close() //nolint: errcheck - bgCancel() - }() - - cp := poolRWC.Get() - defer poolRWC.Put(cp) - - cp.Setup(bgCtx, v.conf.BufferSize, conn, addr) - - for { - if e := internal.Deadline(conn, v.conf.KeepAlive); e != nil { - internal.WriteErrLog("Conn: update keepalive", e, addr) - return - } - - if e := cp.Pickup(); e != nil { - internal.WriteErrLog("Conn: read message", e, addr) - return + internal.Log("Conn: panic", fmt.Errorf("%+v", e), addr) } - if e := internal.Deadline(conn, v.conf.Timeout); e != nil { - internal.WriteErrLog("Conn: update timeout", e, addr) - return - } + stop() - v.handler.Handler(cp) + internal.Log("Conn: close", conn.Close(), addr) + }() - if e := cp.Release(); e != nil { - internal.WriteErrLog("Conn: write message", e, addr) - return - } - } + v.handlerFunc(ctx, conn, conn, addr) }) } } func (v *_server) handlingQUIC(ctx context.Context, l *quic.Listener) error { ctx, cancel := context.WithCancel(ctx) + v.wg.Background(func() { <-ctx.Done() v.close() }) + defer func() { cancel() v.wg.Wait() @@ -281,56 +244,35 @@ func (v *_server) handlingQUIC(ctx context.Context, l *quic.Listener) error { conn, err := l.Accept(ctx) if err != nil { - cancel() + internal.Log("QUIC: accept", err, nil) return err } addr := conn.RemoteAddr() v.wg.Background(func() { - bgCtx, bgCancel := context.WithCancel(ctx) defer func() { if e := recover(); e != nil { - internal.WriteErrLog("QUIC: panic", fmt.Errorf("%+v", e), addr) + internal.Log("QUIC: panic", fmt.Errorf("%+v", e), addr) } - bgCancel() + + //internal.Log("QUIC: close conn", conn.CloseWithError(0, ""), addr) }() - stream, e := conn.AcceptStream(bgCtx) + stream, e := conn.AcceptStream(ctx) if e != nil { - internal.WriteErrLog("QUIC: read message", e, addr) + internal.Log("QUIC: read message", e, addr) return } - defer stream.Close() //nolint: errcheck - cp := poolRWC.Get() - defer poolRWC.Put(cp) + stop := internal.DeadlineUpdate(stream) - cp.Setup(bgCtx, v.conf.BufferSize, stream, addr) - - for { - if e = internal.Deadline(stream, v.conf.KeepAlive); e != nil { - internal.WriteErrLog("QUIC: update keepalive", e, addr) - return - } - - if e = cp.Pickup(); e != nil { - internal.WriteErrLog("QUIC: read message", e, addr) - return - } - - if e = internal.Deadline(stream, v.conf.Timeout); e != nil { - internal.WriteErrLog("QUIC: update timeout", e, addr) - return - } - - v.handler.Handler(cp) + defer func() { + stop() + internal.Log("QUIC: close stream", stream.Close(), addr) + }() - if e = cp.Release(); e != nil { - internal.WriteErrLog("QUIC: write message", e, addr) - return - } - } + v.handlerFunc(ctx, stream, stream, addr) }) } }