diff --git a/README.md b/README.md index bef1fb88ea4..9d8846a4e46 100644 --- a/README.md +++ b/README.md @@ -493,7 +493,7 @@ Metadata flags: - :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention. Logging flags: -- :whale: `--log-driver=(json-file|journald|fluentd)`: Logging driver for the container (default `json-file`). +- :whale: `--log-driver=(json-file|journald|fluentd|syslog)`: Logging driver for the container (default `json-file`). - :whale: `--log-driver=json-file`: The logs are formatted as JSON. The default logging driver for nerdctl. - The `json-file` logging driver supports the following logging options: - :whale: `--log-opt=max-size=`: The maximum size of the log before it is rolled. A positive integer plus a modifier representing the unit of measure (k, m, or g). Defaults to unlimited. @@ -510,6 +510,37 @@ Logging flags: - :whale: `--log-opt=fluentd-sub-second-precision=`: Enable sub-second precision for fluentd. The default value is false. - :nerd_face: `--log-opt=fluentd-async-reconnect-interval=<1s|1ms>`: The time to wait before retrying to reconnect to fluentd. The default value is 0s. - :nerd_face: `--log-opt=fluentd-request-ack=`: Enable request ack for fluentd. The default value is false. + - :whale: `--log-driver=syslog`: Writes log messages to `syslog`. The + `syslog` daemon must be running on either the host machine or remote. + - The `syslog` logging driver supports the following logging options: + - :whale: `--log-opt=syslog-address=
`: The address of an + external `syslog` server. The URI specifier may be + `tcp|udp|tcp+tls]://host:port`, `unix://path`, or `unixgram://path`. + If the transport is `tcp`, `udp`, or `tcp+tls`, the default port is + `514`. + - :whale: `--log-opt=syslog-facility=`: The `syslog` facility to + use. Can be the number or name for any valid syslog facility. See the + [syslog documentation](https://www.rfc-editor.org/rfc/rfc5424#section-6.2.1). + - :whale: `--log-opt=syslog-tls-ca-cert=`: The absolute path to + the trust certificates signed by the CA. **Ignored if the address + protocol is not `tcp+tls`**. + - :whale: `--log-opt=syslog-tls-cert=`: The absolute path to + the TLS certificate file. **Ignored if the address protocol is not + `tcp+tls`**. + - :whale: `--log-opt=syslog-tls-key=`:The absolute path to + the TLS key file. **Ignored if the address protocol is not `tcp+tls`**. + - :whale: `--log-opt=syslog-tls-skip-verify=`: If set to `true`, + TLS verification is skipped when connecting to the daemon. + **Ignored if the address protocol is not `tcp+tls`**. + - :whale: `--log-opt=syslog-format=`: The `syslog` message format + to use. If not specified the local UNIX syslog format is used, + without a specified hostname. Specify `rfc3164` for the RFC-3164 + compatible format, `rfc5424` for RFC-5424 compatible format, or + `rfc5424micro` for RFC-5424 compatible format with microsecond + timestamp resolution. + - :whale: `--log-opt=tag=`: A string that is appended to the + `APP-NAME` in the `syslog` message. By default, nerdctl uses the first + 12 characters of the container ID to tag log messages. - :nerd_face: Accepts a LogURI which is a containerd shim logger. A scheme must be specified for the URI. Example: `nerdctl run -d --log-driver binary:///usr/bin/ctr-journald-shim docker.io/library/hello-world:latest`. An implementation of shim logger can be found at (https://github.com/containerd/containerd/tree/dbef1d56d7ebc05bc4553d72c419ed5ce025b05d/runtime/v2#logging) diff --git a/cmd/nerdctl/run_log_driver_syslog_test.go b/cmd/nerdctl/run_log_driver_syslog_test.go new file mode 100644 index 00000000000..e85a8c1a2a5 --- /dev/null +++ b/cmd/nerdctl/run_log_driver_syslog_test.go @@ -0,0 +1,253 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/containerd/nerdctl/pkg/testutil" + "github.com/containerd/nerdctl/pkg/testutil/testca" + "github.com/containerd/nerdctl/pkg/testutil/testsyslog" + syslog "github.com/yuchanns/srslog" +) + +func runSyslogTest(t *testing.T, networks []string, syslogFacilities map[string]syslog.Priority, fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error) { + base := testutil.NewBase(t) + base.Cmd("pull", testutil.CommonImage).AssertOK() + hostname, err := os.Hostname() + if err != nil { + t.Fatalf("Error retrieving hostname") + } + ca := testca.New(base.T) + cert := ca.NewCert("127.0.0.1") + t.Cleanup(func() { + cert.Close() + ca.Close() + }) + rI := 0 + for _, network := range networks { + for rFK, rFV := range syslogFacilities { + fPriV := rFV + // test both string and number facility + for _, fPriK := range []string{rFK, fmt.Sprintf("%d", int(fPriV)>>3)} { + for fmtK, fmtValidFunc := range fmtValidFuncs { + fmtKT := "empty" + if fmtK != "" { + fmtKT = fmtK + } + subTestName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) + i := rI + rI++ + t.Run(subTestName, func(t *testing.T) { + tID := testutil.Identifier(t) + tag := tID + "_syslog_driver" + msg := "hello, " + tID + "_syslog_driver" + if !testsyslog.TestableNetwork(network) { + if rootlessutil.IsRootless() { + t.Skipf("skipping on %s/%s; '%s' for rootless containers are not supported", runtime.GOOS, runtime.GOARCH, network) + } + t.Skipf("skipping on %s/%s; '%s' is not supported", runtime.GOOS, runtime.GOARCH, network) + } + testContainerName := fmt.Sprintf("%s-%d-%s", tID, i, fPriK) + done := make(chan string) + addr, closer := testsyslog.StartServer(network, "", done, cert) + args := []string{ + "run", + "-d", + "--name", + testContainerName, + "--restart=no", + "--log-driver=syslog", + "--log-opt=syslog-facility=" + fPriK, + "--log-opt=tag=" + tag, + "--log-opt=syslog-format=" + fmtK, + "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", network, addr), + } + if network == "tcp+tls" { + args = append(args, + "--log-opt=syslog-tls-cert="+cert.CertPath, + "--log-opt=syslog-tls-key="+cert.KeyPath, + "--log-opt=syslog-tls-ca-cert="+ca.CertPath, + ) + } + args = append(args, testutil.CommonImage, "echo", msg) + base.Cmd(args...).AssertOK() + t.Cleanup(func() { + base.Cmd("rm", "-f", testContainerName).AssertOK() + }) + defer closer.Close() + defer close(done) + select { + case rcvd := <-done: + if err := fmtValidFunc(rcvd, msg, tag, hostname, fPriV, network == "tcp+tls"); err != nil { + t.Error(err) + } + case <-time.Tick(time.Second * 3): + t.Errorf("timeout with %s", subTestName) + } + }) + } + } + } + } +} + +func TestSyslogNetwork(t *testing.T) { + var syslogFacilities = map[string]syslog.Priority{ + "user": syslog.LOG_USER, + } + + networks := []string{ + "udp", + "tcp", + "tcp+tls", + "unix", + "unixgram", + } + fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ + "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, timestamp string + var length, version, pid int + if !isTLS { + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } else { + exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } + return nil + }, + } + runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) +} + +func TestSyslogFacilities(t *testing.T) { + var syslogFacilities = map[string]syslog.Priority{ + "kern": syslog.LOG_KERN, + "user": syslog.LOG_USER, + "mail": syslog.LOG_MAIL, + "daemon": syslog.LOG_DAEMON, + "auth": syslog.LOG_AUTH, + "syslog": syslog.LOG_SYSLOG, + "lpr": syslog.LOG_LPR, + "news": syslog.LOG_NEWS, + "uucp": syslog.LOG_UUCP, + "cron": syslog.LOG_CRON, + "authpriv": syslog.LOG_AUTHPRIV, + "ftp": syslog.LOG_FTP, + "local0": syslog.LOG_LOCAL0, + "local1": syslog.LOG_LOCAL1, + "local2": syslog.LOG_LOCAL2, + "local3": syslog.LOG_LOCAL3, + "local4": syslog.LOG_LOCAL4, + "local5": syslog.LOG_LOCAL5, + "local6": syslog.LOG_LOCAL6, + "local7": syslog.LOG_LOCAL7, + } + + networks := []string{"unix"} + fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ + "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, timestamp string + var length, version, pid int + if !isTLS { + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } else { + exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } + return nil + }, + } + runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) +} + +func TestSyslogFormat(t *testing.T) { + var syslogFacilities = map[string]syslog.Priority{ + "user": syslog.LOG_USER, + } + + networks := []string{"unix"} + fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ + "": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isSTLS bool) error { + var mon, day, hrs string + var pid int + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s " + tag + "[%d]: " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &pid); n != 4 || err != nil { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil + }, + "rfc3164": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, mon, day, hrs string + var pid int + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s %s " + tag + "[%d]: " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil + }, + "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, timestamp string + var length, version, pid int + if !isTLS { + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } else { + exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } + return nil + }, + "rfc5424micro": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, timestamp string + var length, version, pid int + if !isTLS { + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } else { + exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + } + return nil + }, + } + runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) +} diff --git a/go.mod b/go.mod index 45a1f7c7e5f..b1882f2a078 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/tidwall/gjson v1.14.3 github.com/vishvananda/netlink v1.2.1-beta.2 github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 + github.com/yuchanns/srslog v1.1.0 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/net v0.0.0-20220615171555-694bf12d69de golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 diff --git a/go.sum b/go.sum index 43677b61038..066c785f2e5 100644 --- a/go.sum +++ b/go.sum @@ -1510,6 +1510,8 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk= +github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go index 89efd2c89cc..31f247606a2 100644 --- a/pkg/logging/logging.go +++ b/pkg/logging/logging.go @@ -87,6 +87,9 @@ func init() { RegisterDriver("fluentd", func(opts map[string]string) (Driver, error) { return &FluentdLogger{Opts: opts}, nil }, FluentdLogOptsValidate) + RegisterDriver("syslog", func(opts map[string]string) (Driver, error) { + return &SyslogLogger{Opts: opts}, nil + }, SyslogOptsValidate) } // Main is the entrypoint for the containerd runtime v2 logging plugin mode. diff --git a/pkg/logging/syslog_logger.go b/pkg/logging/syslog_logger.go new file mode 100644 index 00000000000..5fd63fff558 --- /dev/null +++ b/pkg/logging/syslog_logger.go @@ -0,0 +1,272 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package logging + +import ( + "bufio" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/url" + "os" + "strconv" + "strings" + "sync" + + "github.com/docker/go-connections/tlsconfig" + syslog "github.com/yuchanns/srslog" + + "github.com/containerd/containerd/runtime/v2/logging" + "github.com/containerd/nerdctl/pkg/strutil" + "github.com/sirupsen/logrus" +) + +const ( + syslogAddress = "syslog-address" + syslogFacility = "syslog-facility" + syslogTLSCaCert = "syslog-tls-ca-cert" + syslogTLSCert = "syslog-tls-cert" + syslogTLSKey = "syslog-tls-key" + syslogTLSSkipVerify = "syslog-tls-skip-verify" + syslogFormat = "syslog-format" +) + +var syslogOpts = []string{ + syslogAddress, + syslogFacility, + syslogTLSCaCert, + syslogTLSCert, + syslogTLSKey, + syslogTLSSkipVerify, + syslogFormat, + Tag, +} + +var syslogFacilities = map[string]syslog.Priority{ + "kern": syslog.LOG_KERN, + "user": syslog.LOG_USER, + "mail": syslog.LOG_MAIL, + "daemon": syslog.LOG_DAEMON, + "auth": syslog.LOG_AUTH, + "syslog": syslog.LOG_SYSLOG, + "lpr": syslog.LOG_LPR, + "news": syslog.LOG_NEWS, + "uucp": syslog.LOG_UUCP, + "cron": syslog.LOG_CRON, + "authpriv": syslog.LOG_AUTHPRIV, + "ftp": syslog.LOG_FTP, + "local0": syslog.LOG_LOCAL0, + "local1": syslog.LOG_LOCAL1, + "local2": syslog.LOG_LOCAL2, + "local3": syslog.LOG_LOCAL3, + "local4": syslog.LOG_LOCAL4, + "local5": syslog.LOG_LOCAL5, + "local6": syslog.LOG_LOCAL6, + "local7": syslog.LOG_LOCAL7, +} + +const ( + syslogSecureProto = "tcp+tls" + syslogDefaultPort = "514" + + syslogFormatRFC3164 = "rfc3164" + syslogFormatRFC5424 = "rfc5424" + syslogFormatRFC5424Micro = "rfc5424micro" +) + +func SyslogOptsValidate(logOptMap map[string]string) error { + for key := range logOptMap { + if !strutil.InStringSlice(syslogOpts, key) { + logrus.Warnf("log-opt %s is ignored for syslog log driver", key) + } + } + proto, _, err := parseSyslogAddress(logOptMap[syslogAddress]) + if err != nil { + return err + } + if _, err := parseSyslogFacility(logOptMap[syslogFacility]); err != nil { + return err + } + if _, _, err := parseSyslogLogFormat(logOptMap[syslogFormat], proto); err != nil { + return err + } + if proto == syslogSecureProto { + if _, tlsErr := parseTLSConfig(logOptMap); tlsErr != nil { + return tlsErr + } + } + return nil +} + +type SyslogLogger struct { + Opts map[string]string +} + +func (sy *SyslogLogger) Init(dataStore string, ns string, id string) error { + return nil +} + +func (sy *SyslogLogger) Process(dataStore string, config *logging.Config) error { + logger, err := parseSyslog(config.ID, sy.Opts) + if err != nil { + return err + } + defer logger.Close() + var wg sync.WaitGroup + wg.Add(2) + fn := func(r io.Reader, logFn func(msg string) error) { + defer wg.Done() + s := bufio.NewScanner(r) + for s.Scan() { + if s.Err() != nil { + return + } + logFn(s.Text()) + } + } + go fn(config.Stdout, logger.Info) + go fn(config.Stderr, logger.Err) + wg.Wait() + return nil +} + +func parseSyslog(containerID string, config map[string]string) (*syslog.Writer, error) { + tag := containerID[:12] + if cfgTag, ok := config[Tag]; ok { + tag = cfgTag + } + proto, address, err := parseSyslogAddress(config[syslogAddress]) + if err != nil { + return nil, err + } + facility, err := parseSyslogFacility(config[syslogFacility]) + if err != nil { + return nil, err + } + syslogFormatter, syslogFramer, err := parseSyslogLogFormat(config[syslogFormat], proto) + if err != nil { + return nil, err + } + var logger *syslog.Writer + if proto == syslogSecureProto { + tlsConfig, tlsErr := parseTLSConfig(config) + if tlsErr != nil { + return nil, tlsErr + } + logger, err = syslog.DialWithTLSConfig(proto, address, facility, tag, tlsConfig) + } else { + logger, err = syslog.Dial(proto, address, facility, tag) + } + + if err != nil { + return nil, err + } + + logger.SetFormatter(syslogFormatter) + logger.SetFramer(syslogFramer) + + return logger, nil +} + +func parseSyslogAddress(address string) (string, string, error) { + if address == "" { + // Docker-compatible: fallback to `unix:///dev/log`, + // `unix:///var/run/syslog` or `unix:///var/run/log`. We do nothing + // with the empty address, just leave it here and the srslog will + // handle the fallback. + return "", "", nil + } + addr, err := url.Parse(address) + if err != nil { + return "", "", err + } + + // unix and unixgram socket validation + if addr.Scheme == "unix" || addr.Scheme == "unixgram" { + if _, err := os.Stat(addr.Path); err != nil { + return "", "", err + } + return addr.Scheme, addr.Path, nil + } + if addr.Scheme != "udp" && addr.Scheme != "tcp" && addr.Scheme != syslogSecureProto { + return "", "", fmt.Errorf("unsupported scheme: '%s'", addr.Scheme) + } + + // here we process tcp|udp + host := addr.Host + if _, _, err := net.SplitHostPort(host); err != nil { + if !strings.Contains(err.Error(), "missing port in address") { + return "", "", err + } + host = net.JoinHostPort(host, syslogDefaultPort) + } + + return addr.Scheme, host, nil +} + +func parseSyslogFacility(facility string) (syslog.Priority, error) { + if facility == "" { + return syslog.LOG_DAEMON, nil + } + + if syslogFacility, valid := syslogFacilities[facility]; valid { + return syslogFacility, nil + } + + fInt, err := strconv.Atoi(facility) + if err == nil && 0 <= fInt && fInt <= 23 { + return syslog.Priority(fInt << 3), nil + } + + return syslog.Priority(0), errors.New("invalid syslog facility") +} + +func parseTLSConfig(cfg map[string]string) (*tls.Config, error) { + _, skipVerify := cfg[syslogTLSSkipVerify] + + opts := tlsconfig.Options{ + CAFile: cfg[syslogTLSCaCert], + CertFile: cfg[syslogTLSCert], + KeyFile: cfg[syslogTLSKey], + InsecureSkipVerify: skipVerify, + } + + return tlsconfig.Client(opts) +} + +func parseSyslogLogFormat(logFormat, proto string) (syslog.Formatter, syslog.Framer, error) { + switch logFormat { + case "": + return syslog.UnixFormatter, syslog.DefaultFramer, nil + case syslogFormatRFC3164: + return syslog.RFC3164Formatter, syslog.DefaultFramer, nil + case syslogFormatRFC5424: + if proto == syslogSecureProto { + return syslog.RFC5424FormatterWithAppNameAsTag, syslog.RFC5425MessageLengthFramer, nil + } + return syslog.RFC5424FormatterWithAppNameAsTag, syslog.DefaultFramer, nil + case syslogFormatRFC5424Micro: + if proto == syslogSecureProto { + return syslog.RFC5424MicroFormatterWithAppNameAsTag, syslog.RFC5425MessageLengthFramer, nil + } + return syslog.RFC5424MicroFormatterWithAppNameAsTag, syslog.DefaultFramer, nil + default: + return nil, nil, errors.New("invalid syslog format") + } +} diff --git a/pkg/testutil/testsyslog/testsyslog.go b/pkg/testutil/testsyslog/testsyslog.go new file mode 100644 index 00000000000..8d7b9aa6ef8 --- /dev/null +++ b/pkg/testutil/testsyslog/testsyslog.go @@ -0,0 +1,153 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package testsyslog + +import ( + "bufio" + "crypto/tls" + "io" + "log" + "net" + "os" + "runtime" + "time" + + "github.com/containerd/nerdctl/pkg/rootlessutil" + "github.com/containerd/nerdctl/pkg/testutil/testca" +) + +func StartServer(n, la string, done chan<- string, certs ...*testca.Cert) (addr string, sock io.Closer) { + if n == "udp" || n == "tcp" || n == "tcp+tls" { + la = "127.0.0.1:0" + } else { + // unix and unixgram: choose an address if none given + if la == "" { + // use os.CreateTemp to get a name that is unique + f, err := os.CreateTemp("", "syslogtest") + if err != nil { + log.Fatal("TempFile: ", err) + } + f.Close() + la = f.Name() + } + os.Remove(la) + } + + if n == "udp" || n == "unixgram" { + l, e := net.ListenPacket(n, la) + if e != nil { + log.Fatalf("startServer failed: %v", e) + } + addr = l.LocalAddr().String() + sock = l + go runPacketSyslog(l, done) + } else if n == "tcp+tls" { + if len(certs) == 0 { + log.Fatalf("certificates required.") + } + cer := certs[0] + if cer == nil { + log.Fatalf("certificates is nil") + } + cert, err := tls.LoadX509KeyPair(cer.CertPath, cer.KeyPath) + if err != nil { + log.Fatalf("failed to load TLS keypair: %v", err) + } + config := tls.Config{Certificates: []tls.Certificate{cert}} + l, e := tls.Listen("tcp", la, &config) + if e != nil { + log.Fatalf("startServer failed: %v", e) + } + addr = l.Addr().String() + sock = l + go runStreamSyslog(l, done) + } else { + l, e := net.Listen(n, la) + if e != nil { + log.Fatalf("startServer failed: %v", e) + } + addr = l.Addr().String() + sock = l + go runStreamSyslog(l, done) + } + return addr, sock +} + +func TestableNetwork(network string) bool { + switch network { + case "unix", "unixgram": + switch runtime.GOOS { + case "darwin": + switch runtime.GOARCH { + case "arm", "arm64": + return false + } + case "windows": + return false + } + case "udp", "tcp", "tcp+tls": + return !rootlessutil.IsRootless() + } + return true +} + +func runPacketSyslog(c net.PacketConn, done chan<- string) { + var buf [4096]byte + var rcvd string + ct := 0 + for { + var n int + var err error + + _ = c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + n, _, err = c.ReadFrom(buf[:]) + rcvd += string(buf[:n]) + if err != nil { + if oe, ok := err.(*net.OpError); ok { + if ct < 3 && oe.Temporary() { + ct++ + continue + } + } + break + } + } + c.Close() + done <- rcvd +} + +func runStreamSyslog(l net.Listener, done chan<- string) { + for { + var c net.Conn + var err error + if c, err = l.Accept(); err != nil { + return + } + go func(c net.Conn) { + _ = c.SetReadDeadline(time.Now().Add(5 * time.Second)) + b := bufio.NewReader(c) + for ct := 1; ct&7 != 0; ct++ { + s, err := b.ReadString('\n') + if err != nil { + break + } + done <- s + } + c.Close() + }(c) + } +}