2019-01-18 01:50:10 +00:00
|
|
|
// Copyright 2019 the Kilo 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 iptables
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2022-01-04 10:56:29 +00:00
|
|
|
"io"
|
2019-01-18 01:50:10 +00:00
|
|
|
"net"
|
2022-01-04 10:56:29 +00:00
|
|
|
"os"
|
2019-01-18 01:50:10 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/coreos/go-iptables/iptables"
|
2021-02-22 12:07:00 +00:00
|
|
|
"github.com/go-kit/kit/log"
|
|
|
|
"github.com/go-kit/kit/log/level"
|
2019-01-18 01:50:10 +00:00
|
|
|
)
|
|
|
|
|
2022-01-04 10:56:29 +00:00
|
|
|
const ipv6ModuleDisabledPath = "/sys/module/ipv6/parameters/disable"
|
|
|
|
|
|
|
|
func ipv6Disabled() (bool, error) {
|
|
|
|
f, err := os.Open(ipv6ModuleDisabledPath)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
disabled := make([]byte, 1)
|
|
|
|
if _, err = io.ReadFull(f, disabled); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return disabled[0] == '1', nil
|
|
|
|
}
|
|
|
|
|
2020-03-12 14:48:01 +00:00
|
|
|
// Protocol represents an IP protocol.
|
|
|
|
type Protocol byte
|
|
|
|
|
|
|
|
const (
|
|
|
|
// ProtocolIPv4 represents the IPv4 protocol.
|
|
|
|
ProtocolIPv4 Protocol = iota
|
|
|
|
// ProtocolIPv6 represents the IPv6 protocol.
|
|
|
|
ProtocolIPv6
|
|
|
|
)
|
|
|
|
|
|
|
|
// GetProtocol will return a protocol from the length of an IP address.
|
|
|
|
func GetProtocol(length int) Protocol {
|
|
|
|
if length == net.IPv6len {
|
|
|
|
return ProtocolIPv6
|
|
|
|
}
|
|
|
|
return ProtocolIPv4
|
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
// Client represents any type that can administer iptables rules.
|
|
|
|
type Client interface {
|
|
|
|
AppendUnique(table string, chain string, rule ...string) error
|
|
|
|
Delete(table string, chain string, rule ...string) error
|
|
|
|
Exists(table string, chain string, rule ...string) (bool, error)
|
2021-02-16 13:00:07 +00:00
|
|
|
List(table string, chain string) ([]string, error)
|
2020-02-20 11:24:52 +00:00
|
|
|
ClearChain(table string, chain string) error
|
|
|
|
DeleteChain(table string, chain string) error
|
|
|
|
NewChain(table string, chain string) error
|
2021-02-16 13:00:07 +00:00
|
|
|
ListChains(table string) ([]string, error)
|
2020-02-20 11:24:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Rule is an interface for interacting with iptables objects.
|
|
|
|
type Rule interface {
|
|
|
|
Add(Client) error
|
|
|
|
Delete(Client) error
|
|
|
|
Exists(Client) (bool, error)
|
|
|
|
String() string
|
2020-03-12 14:48:01 +00:00
|
|
|
Proto() Protocol
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// rule represents an iptables rule.
|
|
|
|
type rule struct {
|
2020-02-20 11:24:52 +00:00
|
|
|
table string
|
|
|
|
chain string
|
|
|
|
spec []string
|
2020-03-12 14:48:01 +00:00
|
|
|
proto Protocol
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
2020-03-12 14:48:01 +00:00
|
|
|
// NewRule creates a new iptables or ip6tables rule in the given table and chain
|
|
|
|
// depending on the given protocol.
|
|
|
|
func NewRule(proto Protocol, table, chain string, spec ...string) Rule {
|
|
|
|
return &rule{table, chain, spec, proto}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewIPv4Rule creates a new iptables rule in the given table and chain.
|
|
|
|
func NewIPv4Rule(table, chain string, spec ...string) Rule {
|
|
|
|
return &rule{table, chain, spec, ProtocolIPv4}
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewIPv6Rule creates a new ip6tables rule in the given table and chain.
|
|
|
|
func NewIPv6Rule(table, chain string, spec ...string) Rule {
|
|
|
|
return &rule{table, chain, spec, ProtocolIPv6}
|
2020-03-06 15:57:05 +00:00
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
func (r *rule) Add(client Client) error {
|
|
|
|
if err := client.AppendUnique(r.table, r.chain, r.spec...); err != nil {
|
2019-01-18 01:50:10 +00:00
|
|
|
return fmt.Errorf("failed to add iptables rule: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
func (r *rule) Delete(client Client) error {
|
2019-01-18 01:50:10 +00:00
|
|
|
// Ignore the returned error as an error likely means
|
|
|
|
// that the rule doesn't exist, which is fine.
|
2020-02-20 11:24:52 +00:00
|
|
|
client.Delete(r.table, r.chain, r.spec...)
|
2019-01-18 01:50:10 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
func (r *rule) Exists(client Client) (bool, error) {
|
|
|
|
return client.Exists(r.table, r.chain, r.spec...)
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *rule) String() string {
|
|
|
|
if r == nil {
|
|
|
|
return ""
|
|
|
|
}
|
2021-02-16 13:00:07 +00:00
|
|
|
spec := r.table + " -A " + r.chain
|
|
|
|
for i, s := range r.spec {
|
|
|
|
spec += " "
|
|
|
|
// If this is the content of a comment, wrap the value in quotes.
|
|
|
|
if i > 0 && r.spec[i-1] == "--comment" {
|
|
|
|
spec += `"` + s + `"`
|
|
|
|
} else {
|
|
|
|
spec += s
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return spec
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
2020-03-12 14:48:01 +00:00
|
|
|
func (r *rule) Proto() Protocol {
|
|
|
|
return r.proto
|
|
|
|
}
|
|
|
|
|
2019-01-18 01:50:10 +00:00
|
|
|
// chain represents an iptables chain.
|
|
|
|
type chain struct {
|
2020-02-20 11:24:52 +00:00
|
|
|
table string
|
|
|
|
chain string
|
2020-03-12 14:48:01 +00:00
|
|
|
proto Protocol
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewIPv4Chain creates a new iptables chain in the given table.
|
|
|
|
func NewIPv4Chain(table, name string) Rule {
|
|
|
|
return &chain{table, name, ProtocolIPv4}
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
2020-03-12 14:48:01 +00:00
|
|
|
// NewIPv6Chain creates a new ip6tables chain in the given table.
|
|
|
|
func NewIPv6Chain(table, name string) Rule {
|
|
|
|
return &chain{table, name, ProtocolIPv6}
|
2020-03-06 15:57:05 +00:00
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
func (c *chain) Add(client Client) error {
|
2021-02-16 13:00:07 +00:00
|
|
|
// Note: `ClearChain` creates a chain if it does not exist.
|
2020-02-20 11:24:52 +00:00
|
|
|
if err := client.ClearChain(c.table, c.chain); err != nil {
|
2019-01-18 01:50:10 +00:00
|
|
|
return fmt.Errorf("failed to add iptables chain: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
func (c *chain) Delete(client Client) error {
|
2019-01-18 01:50:10 +00:00
|
|
|
// The chain must be empty before it can be deleted.
|
2020-02-20 11:24:52 +00:00
|
|
|
if err := client.ClearChain(c.table, c.chain); err != nil {
|
2019-01-18 01:50:10 +00:00
|
|
|
return fmt.Errorf("failed to clear iptables chain: %v", err)
|
|
|
|
}
|
|
|
|
// Ignore the returned error as an error likely means
|
|
|
|
// that the chain doesn't exist, which is fine.
|
2020-02-20 11:24:52 +00:00
|
|
|
client.DeleteChain(c.table, c.chain)
|
2019-01-18 01:50:10 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-20 11:24:52 +00:00
|
|
|
func (c *chain) Exists(client Client) (bool, error) {
|
2019-01-18 01:50:10 +00:00
|
|
|
// The code for "chain already exists".
|
|
|
|
existsErr := 1
|
2020-02-20 11:24:52 +00:00
|
|
|
err := client.NewChain(c.table, c.chain)
|
2019-01-18 01:50:10 +00:00
|
|
|
se, ok := err.(statusExiter)
|
|
|
|
switch {
|
|
|
|
case err == nil:
|
|
|
|
// If there was no error adding a new chain, then it did not exist.
|
|
|
|
// Delete it and return false.
|
2020-02-20 11:24:52 +00:00
|
|
|
client.DeleteChain(c.table, c.chain)
|
2019-01-18 01:50:10 +00:00
|
|
|
return false, nil
|
|
|
|
case ok && se.ExitStatus() == existsErr:
|
|
|
|
return true, nil
|
|
|
|
default:
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *chain) String() string {
|
|
|
|
if c == nil {
|
|
|
|
return ""
|
|
|
|
}
|
2021-02-16 13:00:07 +00:00
|
|
|
return chainToString(c.table, c.chain)
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
2020-03-12 14:48:01 +00:00
|
|
|
func (c *chain) Proto() Protocol {
|
|
|
|
return c.proto
|
|
|
|
}
|
|
|
|
|
2021-02-16 13:00:07 +00:00
|
|
|
func chainToString(table, chain string) string {
|
|
|
|
return fmt.Sprintf("%s -N %s", table, chain)
|
|
|
|
}
|
|
|
|
|
2019-01-18 01:50:10 +00:00
|
|
|
// Controller is able to reconcile a given set of iptables rules.
|
|
|
|
type Controller struct {
|
2021-02-28 17:33:01 +00:00
|
|
|
v4 Client
|
|
|
|
v6 Client
|
|
|
|
errors chan error
|
|
|
|
logger log.Logger
|
|
|
|
resyncPeriod time.Duration
|
2019-09-25 11:23:18 +00:00
|
|
|
|
|
|
|
sync.Mutex
|
|
|
|
rules []Rule
|
2019-01-18 01:50:10 +00:00
|
|
|
subscribed bool
|
|
|
|
}
|
|
|
|
|
2021-02-22 12:07:00 +00:00
|
|
|
// ControllerOption modifies the controller's configuration.
|
|
|
|
type ControllerOption func(h *Controller)
|
|
|
|
|
|
|
|
// WithLogger adds a logger to the controller.
|
|
|
|
func WithLogger(logger log.Logger) ControllerOption {
|
|
|
|
return func(c *Controller) {
|
|
|
|
c.logger = logger
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
2021-02-22 12:07:00 +00:00
|
|
|
}
|
|
|
|
|
2021-02-28 17:33:01 +00:00
|
|
|
// WithResyncPeriod modifies how often the controller reconciles.
|
|
|
|
func WithResyncPeriod(resyncPeriod time.Duration) ControllerOption {
|
|
|
|
return func(c *Controller) {
|
|
|
|
c.resyncPeriod = resyncPeriod
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-22 12:07:00 +00:00
|
|
|
// WithClients adds iptables clients to the controller.
|
|
|
|
func WithClients(v4, v6 Client) ControllerOption {
|
|
|
|
return func(c *Controller) {
|
|
|
|
c.v4 = v4
|
|
|
|
c.v6 = v6
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
2021-02-22 12:07:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// New generates a new iptables rules controller.
|
|
|
|
// If no options are given, IPv4 and IPv6 clients
|
|
|
|
// will be instantiated using the regular iptables backend.
|
|
|
|
func New(opts ...ControllerOption) (*Controller, error) {
|
|
|
|
c := &Controller{
|
2019-01-18 01:50:10 +00:00
|
|
|
errors: make(chan error),
|
2021-02-22 12:07:00 +00:00
|
|
|
logger: log.NewNopLogger(),
|
|
|
|
}
|
|
|
|
for _, o := range opts {
|
|
|
|
o(c)
|
|
|
|
}
|
|
|
|
if c.v4 == nil {
|
|
|
|
v4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create iptables IPv4 client: %v", err)
|
|
|
|
}
|
|
|
|
c.v4 = v4
|
|
|
|
}
|
|
|
|
if c.v6 == nil {
|
2022-01-04 10:56:29 +00:00
|
|
|
disabled, err := ipv6Disabled()
|
2021-02-22 12:07:00 +00:00
|
|
|
if err != nil {
|
2022-01-04 10:56:29 +00:00
|
|
|
return nil, fmt.Errorf("failed to check IPv6 status: %v", err)
|
|
|
|
}
|
|
|
|
if disabled {
|
|
|
|
level.Info(c.logger).Log("msg", "IPv6 is disabled in the kernel; disabling the IPv6 iptables controller")
|
|
|
|
c.v6 = &fakeClient{}
|
|
|
|
} else {
|
|
|
|
v6, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to create iptables IPv6 client: %v", err)
|
|
|
|
}
|
|
|
|
c.v6 = v6
|
2021-02-22 12:07:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return c, nil
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run watches for changes to iptables rules and reconciles
|
|
|
|
// the rules against the desired state.
|
|
|
|
func (c *Controller) Run(stop <-chan struct{}) (<-chan error, error) {
|
2019-09-25 11:23:18 +00:00
|
|
|
c.Lock()
|
2019-01-18 01:50:10 +00:00
|
|
|
if c.subscribed {
|
2019-09-25 11:23:18 +00:00
|
|
|
c.Unlock()
|
2019-01-18 01:50:10 +00:00
|
|
|
return c.errors, nil
|
|
|
|
}
|
|
|
|
// Ensure a given instance only subscribes once.
|
|
|
|
c.subscribed = true
|
2019-09-25 11:23:18 +00:00
|
|
|
c.Unlock()
|
2019-01-18 01:50:10 +00:00
|
|
|
go func() {
|
2021-02-28 17:33:01 +00:00
|
|
|
t := time.NewTimer(c.resyncPeriod)
|
2019-01-18 01:50:10 +00:00
|
|
|
defer close(c.errors)
|
|
|
|
for {
|
|
|
|
select {
|
2021-02-28 17:33:01 +00:00
|
|
|
case <-t.C:
|
|
|
|
if err := c.reconcile(); err != nil {
|
|
|
|
nonBlockingSend(c.errors, fmt.Errorf("failed to reconcile rules: %v", err))
|
|
|
|
}
|
|
|
|
t.Reset(c.resyncPeriod)
|
2019-01-18 01:50:10 +00:00
|
|
|
case <-stop:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return c.errors, nil
|
|
|
|
}
|
|
|
|
|
2019-09-25 11:23:18 +00:00
|
|
|
// reconcile makes sure that every rule is still in the backend.
|
|
|
|
// It does not ensure that the order in the backend is correct.
|
|
|
|
// If any rule is missing, that rule and all following rules are
|
|
|
|
// re-added.
|
|
|
|
func (c *Controller) reconcile() error {
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
2021-02-16 13:00:07 +00:00
|
|
|
var rc ruleCache
|
2019-09-25 11:23:18 +00:00
|
|
|
for i, r := range c.rules {
|
2021-02-16 13:00:07 +00:00
|
|
|
ok, err := rc.exists(c.client(r.Proto()), r)
|
2019-09-25 11:23:18 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to check if rule exists: %v", err)
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
2019-09-25 11:23:18 +00:00
|
|
|
if !ok {
|
2021-02-22 12:07:00 +00:00
|
|
|
level.Info(c.logger).Log("msg", fmt.Sprintf("applying %d iptables rules", len(c.rules)-i))
|
2020-02-20 11:24:52 +00:00
|
|
|
if err := c.resetFromIndex(i, c.rules); err != nil {
|
2019-09-25 11:23:18 +00:00
|
|
|
return fmt.Errorf("failed to add rule: %v", err)
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
2019-09-25 11:23:18 +00:00
|
|
|
break
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
}
|
2019-09-25 11:23:18 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// resetFromIndex re-adds all rules starting from the given index.
|
2020-02-20 11:24:52 +00:00
|
|
|
func (c *Controller) resetFromIndex(i int, rules []Rule) error {
|
2019-09-25 11:23:18 +00:00
|
|
|
if i >= len(rules) {
|
|
|
|
return nil
|
|
|
|
}
|
2019-09-27 09:10:15 +00:00
|
|
|
for j := i; j < len(rules); j++ {
|
2020-03-12 14:48:01 +00:00
|
|
|
if err := rules[j].Delete(c.client(rules[j].Proto())); err != nil {
|
2019-09-25 11:23:18 +00:00
|
|
|
return fmt.Errorf("failed to delete rule: %v", err)
|
|
|
|
}
|
2020-03-12 14:48:01 +00:00
|
|
|
if err := rules[j].Add(c.client(rules[j].Proto())); err != nil {
|
2019-09-25 11:23:18 +00:00
|
|
|
return fmt.Errorf("failed to add rule: %v", err)
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-09-25 11:23:18 +00:00
|
|
|
// deleteFromIndex deletes all rules starting from the given index.
|
2020-02-20 11:24:52 +00:00
|
|
|
func (c *Controller) deleteFromIndex(i int, rules *[]Rule) error {
|
2019-09-25 11:23:18 +00:00
|
|
|
if i >= len(*rules) {
|
|
|
|
return nil
|
|
|
|
}
|
2019-09-27 09:10:15 +00:00
|
|
|
for j := i; j < len(*rules); j++ {
|
2020-03-12 14:48:01 +00:00
|
|
|
if err := (*rules)[j].Delete(c.client((*rules)[j].Proto())); err != nil {
|
2020-05-11 20:50:01 +00:00
|
|
|
*rules = append((*rules)[:i], (*rules)[j:]...)
|
2019-01-18 01:50:10 +00:00
|
|
|
return fmt.Errorf("failed to delete rule: %v", err)
|
|
|
|
}
|
2019-09-25 11:23:18 +00:00
|
|
|
(*rules)[j] = nil
|
2019-01-18 01:50:10 +00:00
|
|
|
}
|
2019-09-25 11:23:18 +00:00
|
|
|
*rules = (*rules)[:i]
|
2019-01-18 01:50:10 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-09-25 11:23:18 +00:00
|
|
|
// Set idempotently overwrites any iptables rules previously defined
|
|
|
|
// for the controller with the given set of rules.
|
|
|
|
func (c *Controller) Set(rules []Rule) error {
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
|
|
|
var i int
|
|
|
|
for ; i < len(rules); i++ {
|
|
|
|
if i < len(c.rules) {
|
|
|
|
if rules[i].String() != c.rules[i].String() {
|
2020-02-20 11:24:52 +00:00
|
|
|
if err := c.deleteFromIndex(i, &c.rules); err != nil {
|
2019-09-25 11:23:18 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if i >= len(c.rules) {
|
2020-03-12 14:48:01 +00:00
|
|
|
if err := rules[i].Add(c.client(rules[i].Proto())); err != nil {
|
2019-09-25 11:23:18 +00:00
|
|
|
return fmt.Errorf("failed to add rule: %v", err)
|
|
|
|
}
|
|
|
|
c.rules = append(c.rules, rules[i])
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-02-20 11:24:52 +00:00
|
|
|
return c.deleteFromIndex(i, &c.rules)
|
2019-09-25 11:23:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CleanUp will clean up any rules created by the controller.
|
|
|
|
func (c *Controller) CleanUp() error {
|
|
|
|
c.Lock()
|
|
|
|
defer c.Unlock()
|
2020-02-20 11:24:52 +00:00
|
|
|
return c.deleteFromIndex(0, &c.rules)
|
2019-09-25 11:23:18 +00:00
|
|
|
}
|
|
|
|
|
2020-03-12 14:48:01 +00:00
|
|
|
func (c *Controller) client(p Protocol) Client {
|
|
|
|
switch p {
|
|
|
|
case ProtocolIPv4:
|
|
|
|
return c.v4
|
|
|
|
case ProtocolIPv6:
|
|
|
|
return c.v6
|
|
|
|
default:
|
|
|
|
panic("unknown protocol")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-18 01:50:10 +00:00
|
|
|
func nonBlockingSend(errors chan<- error, err error) {
|
|
|
|
select {
|
|
|
|
case errors <- err:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
}
|