File: helpers.network.bash

package info (click to toggle)
podman 5.7.0%2Bds2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 23,824 kB
  • sloc: sh: 4,700; python: 2,798; perl: 1,885; ansic: 1,484; makefile: 977; ruby: 42; csh: 8
file content (451 lines) | stat: -rw-r--r-- 14,678 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# -*- bash -*-

_cached_has_slirp4netns=

### Feature Checks #############################################################

# has_ipv4() - Check if one default route is available for IPv4
function has_ipv4() {
    [ -n "$(ip -j -4 route show | jq -rM '.[] | select(.dst == "default")')" ]
}

# has_ipv6() - Check if one default route is available for IPv6
function has_ipv6() {
    [ -n "$(ip -j -6 route show | jq -rM '.[] | select(.dst == "default")')" ]
}

# skip_if_no_ipv4() - Skip current test if IPv4 traffic can't be routed
# $1:	Optional message to display
function skip_if_no_ipv4() {
    if ! has_ipv4; then
        local msg=$(_add_label_if_missing "$1" "IPv4")
        skip "${msg:-not applicable with no routable IPv4}"
    fi
}

# skip_if_no_ipv6() - Skip current test if IPv6 traffic can't be routed
# $1:	Optional message to display
function skip_if_no_ipv6() {
    if ! has_ipv6; then
        local msg=$(_add_label_if_missing "$1" "IPv6")
        skip "${msg:-not applicable with no routable IPv6}"
    fi
}

# has_slirp4netns - Check if the slirp4netns(1) command is available
function has_slirp4netns() {
    if [[ -z "$_cached_has_slirp4netns" ]]; then
        _cached_has_slirp4netns=n
        run_podman info --format '{{.Host.Slirp4NetNS.Executable}}'
        if [[ -n "$output" ]]; then
            _cached_has_slirp4netns=y
        fi
    fi
    test "$_cached_has_slirp4netns" = "y"
}

### procfs access ##############################################################

# ipv6_to_procfs() - RFC 5952 IPv6 address text representation to procfs format
# $1:	Address in any notation described by RFC 5952
function ipv6_to_procfs() {
    local addr="${1}"

    # Add leading zero if missing
    case ${addr} in
        "::"*) addr=0"${addr}" ;;
    esac

    # Double colon can mean any number of all-zero fields. Expand to fill
    # as many colons as are missing. (This will not be a valid IPv6 form,
    # but we don't need it for long). E.g., 0::1 -> 0:::::::1
    case ${addr} in
        *"::"*)
            # All the colons in the address
            local colons
            colons=$(tr -dc : <<<$addr)
            # subtract those from a string of eight colons; this gives us
            # a string of two to six colons...
            local pad
            pad=$(sed -e "s/$colons//" <<<":::::::")
            # ...which we then inject in place of the double colon.
            addr=$(sed -e "s/::/::$pad/" <<<$addr)
            ;;
    esac

    # Print as a contiguous string of zero-filled 16-bit words
    # (The additional ":" below is needed because 'read -d x' actually
    # means "x is a TERMINATOR, not a delimiter")
    local group
    while read -d : group; do
        printf "%04X" "0x${group:-0}"
    done <<<"${addr}:"
}

# __ipv4_to_procfs() - Print bytes in hexadecimal notation reversing arguments
# $@:	IPv4 address as separate bytes
function __ipv4_to_procfs() {
    printf "%02X%02X%02X%02X" ${4} ${3} ${2} ${1}
}

# ipv4_to_procfs() - IPv4 address representation to big-endian procfs format
# $1:	Text representation of IPv4 address
function ipv4_to_procfs() {
    IFS='.' read -r o1 o2 o3 o4 <<< $1
    __ipv4_to_procfs $o1 $o2 $o3 $o4
}


### Addresses, Routes, Links ###################################################

# ipv4_get_addr_global() - Print first global IPv4 address reported by netlink
# $1:	Optional output of 'ip -j -4 address show' from a different context
function ipv4_get_addr_global() {
    local expr='[.[].addr_info[] | select(.scope=="global")] | .[0].local'
    echo "${1:-$(ip -j -4 address show)}" | jq -rM "${expr}"
}

# ipv6_get_addr_global() - Print first global IPv6 address reported by netlink
# $1:	Optional output of 'ip -j -6 address show' from a different context
function ipv6_get_addr_global() {
    local expr='[.[].addr_info[] | select(.scope=="global")] | .[0].local'
    echo "${1:-$(ip -j -6 address show)}" | jq -rM "${expr}"
}

# random_rfc1918_subnet() - Pseudorandom unused subnet in 172.16/12 prefix
#
# Use the class B set, because much of our CI environment (Google, RH)
# already uses up much of the class A, and it's really hard to test
# if a block is in use.
#
# This returns THREE OCTETS! It is up to our caller to append .0/24, .255, &c.
#
function random_rfc1918_subnet() {
    local retries=1024

    while [ "$retries" -gt 0 ];do
        # 172.16.0.0 -> 172.31.255.255
        local n1=172
        local n2=$(( 16 + $RANDOM & 15 ))
        local n3=$(( $RANDOM & 255 ))

        if ! subnet_in_use $n1 $n2 $n3; then
            echo "$n1.$n2.$n3"
            return
        fi

        retries=$(( retries - 1 ))
    done

    die "Could not find a random not-in-use rfc1918 subnet"
}

# subnet_in_use() - true if subnet already routed on host
function subnet_in_use() {
    local subnet_script=${PODMAN_TMPDIR-/var/tmp}/subnet-in-use
    rm -f $subnet_script

    # This would be a nightmare to do in bash. ipcalc, ipcalc-ng, sipcalc
    # would be nice but are unavailable some environments (cough RHEL).
    # Likewise python/perl netmask modules. So, use bare-bones perl.
    cat >$subnet_script <<"EOF"
#!/usr/bin/env perl

use strict;
use warnings;

# 3 octets, in binary: 172.16.x -> 1010 1100 0000 1000 xxxx xxxx ...
my $subnet_to_check = sprintf("%08b%08b%08b", @ARGV);

my $found = 0;

# Input is "ip route list", one or more lines like '10.0.0.0/8 via ...'
while (<STDIN>) {
    # Only interested in x.x.x.x/n lines
    if (m!^([\d.]+)/(\d+)!) {
        my ($ip, $bits) = ($1, $2);

        # Our caller has /24 granularity, so treat /30 on host as /24.
        $bits = 24 if $bits > 24;

        # Temporary: entire subnet as binary string. 4 octets, split,
        # then represented as a 32-bit binary string.
        my $net = sprintf("%08b%08b%08b%08b", split(/\./, $ip));

        # Now truncate those 32 bits down to the route's netmask size.
        # This is the actual subnet range in use on the host.
        my $net_truncated = sprintf("%.*s", $bits, $net);

        # Desired subnet is in use if it matches a host route prefix
#        print STDERR "--- $subnet_to_check in $net_truncated (@ARGV in $ip/$bits)\n";
        $found = 1 if $subnet_to_check =~ /^$net_truncated/;
    }
}

# Convert to shell exit status (0 = success)
exit !$found;
EOF

    chmod 755 $subnet_script

    # This runs 'ip route list', converts x.x.x.x/n to its binary prefix,
    # then checks if our desired subnet matches that prefix (i.e. is in
    # that range). Existing routes with size greater than 24 are
    # normalized to /24 because that's the granularity of our
    # random_rfc1918_subnet code.
    #
    # Contrived examples:
    #    127.0.0.0/1   -> 0
    #    128.0.0.0/1   -> 1
    #    10.0.0.0/8    -> 00001010
    #
    # I'm so sorry for the ugliness.
    ip route list | $subnet_script $*
}

# ipv4_get_route_default() - Print first default IPv4 route reported by netlink
# $1:	Optional output of 'ip -j -4 route show' from a different context
function ipv4_get_route_default() {
    local jq_gw='[.[] | select(.dst == "default").gateway] | .[0]'
    local jq_nh='[.[] | select(.dst == "default").nexthops[0].gateway] | .[0]'
    local out

    out="$(echo "${1:-$(ip -j -4 route show)}" | jq -rM "${jq_gw}")"
    if [ "${out}" = "null" ]; then
        out="$(echo "${1:-$(ip -j -4 route show)}" | jq -rM "${jq_nh}")"
    fi

    echo "${out}"
}

# ipv6_get_route_default() - Print first default IPv6 route reported by netlink
# $1:	Optional output of 'ip -j -6 route show' from a different context
function ipv6_get_route_default() {
    local jq_gw='[.[] | select(.dst == "default").gateway] | .[0]'
    local jq_nh='[.[] | select(.dst == "default").nexthops[0].gateway] | .[0]'
    local out

    out="$(echo "${1:-$(ip -j -6 route show)}" | jq -rM "${jq_gw}")"
    if [ "${out}" = "null" ]; then
        out="$(echo "${1:-$(ip -j -6 route show)}" | jq -rM "${jq_nh}")"
    fi

    echo "${out}"
}

# ether_get_mtu() - Get MTU of first Ethernet-like link
# $1:	Optional output of 'ip -j link show' from a different context
function ether_get_mtu() {
    local jq_expr='[.[] | select(.link_type == "ether").mtu] | .[0]'
    echo "${1:-$(ip -j link show)}" | jq -rM "${jq_expr}"
}

# ether_get_name() - Get name of first Ethernet-like interface
# $1:	Optional output of 'ip -j link show' from a different context
function ether_get_name() {
    local jq_expr='[.[] | select(.link_type == "ether").ifname] | .[0]'
    echo "${1:-$(ip -j link show)}" | jq -rM "${jq_expr}"
}


### Ports and Ranges ###########################################################

# reserve_port() - create a lock file reserving a port, or return false
function reserve_port() {
    local port=$1

    mkdir -p $PORT_LOCK_DIR
    local lockfile=$PORT_LOCK_DIR/$port
    local locktmp=$PORT_LOCK_DIR/.$port.$$
    echo $BATS_SUITE_TEST_NUMBER >$locktmp

    if ln $locktmp $lockfile; then
        rm -f $locktmp
        return
    fi
    # Port already reserved
    rm -f $locktmp
    false
}

# unreserve_port() - free a temporarily-reserved port
function unreserve_port() {
    local port=$1

    local lockfile=$PORT_LOCK_DIR/$port
    -e $lockfile || die "Cannot unreserve non-reserved port $port"
    assert "$(< $lockfile)" = "$BATS_SUITE_TEST_NUMBER" \
           "Port $port is not reserved by this test"
    rm -f $lockfile
}


# random_free_port() - Get unbound port with pseudorandom number
# $1:	Optional, dash-separated interval, [5000, 5999] by default
# $2:	Optional binding address, any IPv4 address by default
# $3:	Optional protocol, tcp or udp
function random_free_port() {
    local range=${1:-5000-5999}
    local address=${2:-0.0.0.0}
    local protocol=${3:-tcp}

    local port
    for port in $(shuf -i ${range}); do
        # First make sure no other tests are using it
        if reserve_port $port; then
            if port_is_free $port $address $protocol; then
                echo $port
                return
            fi

            unreserve_port $port
        fi
    done

    die "Could not find open port in range $range"
}

# random_free_port_range() - Get range of unbound ports with pseudorandom start
# $1:	Size of range (i.e. number of ports)
# $2:	Optional binding address, any IPv4 address by default
# $3:	Optional protocol, tcp or udp
function random_free_port_range() {
    local size=${1?Usage: random_free_port_range SIZE [ADDRESS [tcp|udp]]}
    local address=${2:-0.0.0.0}
    local protocol=${3:-tcp}

    local maxtries=10
    while [[ $maxtries -gt 0 ]]; do
        local firstport=$(random_free_port)
        local lastport=
        for i in $(seq 1 $((size - 1))); do
            lastport=$((firstport + i))
            if ! reserve_port $lastport; then
                lastport=
                break
            fi
            if ! port_is_free $lastport $address $protocol; then
                echo "# port $lastport is in use; trying another." >&3
                unreserve_port $lastport
                lastport=
                break
            fi
        done
        if [[ -n "$lastport" ]]; then
            echo "$firstport-$lastport"
            return
        fi

        unreserve_port $firstport

        maxtries=$((maxtries - 1))
    done

    die "Could not find free port range with size $size"
}

# port_is_bound() - Check if TCP or UDP port is bound for a given address
# $1:	Port number
# $2:	Optional protocol, or optional IPv4 or IPv6 address, default: tcp
# $3:	Optional IPv4 or IPv6 address, or optional protocol, default: any
function port_is_bound() {
    local port=${1?Usage: port_is_bound PORT [tcp|udp] [ADDRESS]}

    if   [ "${2}" = "tcp" ] || [ "${2}" = "udp" ]; then
        local address="${3}"
        local proto="${2}"
    elif [ "${3}" = "tcp" ] || [ "${3}" = "udp" ]; then
        local address="${2}"
        local proto="${3}"
    else
        local address="${2}"	# Might be empty
        local proto="tcp"
    fi

    # /proc/net/tcp is insufficient: it does not show some rootless ports.
    # ss does, so check it first.
    run ss -${proto:0:1}nlH sport = $port
    if [[ -n "$output" ]]; then
        return
    fi

    port=$(printf %04X ${port})
    case "${address}" in
    *":"*)
        grep -e "^[^:]*: $(ipv6_to_procfs "${address}"):${port} .*" \
             -e "^[^:]*: $(ipv6_to_procfs "::0"):${port} .*"        \
             -q "/proc/net/${proto}6"
        ;;
    *"."*)
        grep -e "^[^:]*: $(ipv4_to_procfs "${address}"):${port}"    \
             -e "^[^:]*: $(ipv4_to_procfs "0.0.0.0"):${port}"       \
             -e "^[^:]*: $(ipv4_to_procfs "127.0.0.1"):${port}"     \
             -q "/proc/net/${proto}"
        ;;
    *)
        # No address: check both IPv4 and IPv6, for any bound address
        grep "^[^:]*: [^:]*:${port} .*" -q "/proc/net/${proto}6" || \
        grep "^[^:]*: [^:]*:${port} .*" -q "/proc/net/${proto}"
        ;;
    esac
}

# port_is_free() - Check if TCP or UDP port is free to bind for a given address
# $1:	Port number
# $2:	Optional protocol, or optional IPv4 or IPv6 address, default: tcp
# $3:	Optional IPv4 or IPv6 address, or optional protocol, default: any
function port_is_free() {
    ! port_is_bound ${@}
}

# wait_for_port() - Return once port is bound (available for use by clients)
# $1:	Host or address to check for possible binding
# $2:	Port number
# $3:	Optional timeout, 5 seconds if not given
function wait_for_port() {
    local host=$1
    local port=$2
    local _timeout=${3:-5}

    # Wait
    while [ $_timeout -gt 0 ]; do
        port_is_bound ${port} "${host}" && return
        sleep 1
        _timeout=$(( $_timeout - 1 ))
    done

    die "Timed out waiting for $host:$port"
}

# tcp_port_probe() - Check if a TCP port has an active listener
# $1:	Port number
# $2:	Optional address, 0.0.0.0 by default
function tcp_port_probe() {
    local address="${2:-0.0.0.0}"

    (exec echo -n >/dev/tcp/"$address/$1") >/dev/null 2>&1
}

### Pasta Helpers ##############################################################

function default_ifname() {
    local jq_expr='[.[] | select(.dst == "default").dev] | .[0]'
    local jq_expr_nh='[.[] | select(.dst == "default").nexthops[0].dev] | .[0]'
    local ip_ver="${1}"
    local out

    out="$(ip -j -"${ip_ver}" route show | jq -rM "${jq_expr}")"
    if [ "${out}" = "null" ]; then
        out="$(ip -j -"${ip_ver}" route show | jq -rM "${jq_expr_nh}")"
    fi

    echo "${out}"
}

function default_addr() {
    local ip_ver="${1}"
    local ifname="${2:-$(default_ifname "${ip_ver}")}"

    local expr='[.[0].addr_info[] | select(.deprecated != true)][0].local'
    ip -j -"${ip_ver}" addr show "${ifname}" | jq -rM "${expr}"
}