summaryrefslogtreecommitdiff
path: root/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper
blob: bd111d2826da7a812dfe43b1c8ae07fb79ab5bde (plain)
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
# redefine ip command to use FRR when it is available and preserve static addresses

# default route distance
IF_METRIC=${IF_METRIC:-210}

# Check if interface is inside a VRF
VRF_OPTION=$(/usr/sbin/ip --json --detail link show ${interface} | jq -r '.[0] | select(.linkinfo.info_slave_kind == "vrf") | "vrf \(.master)"')

# Flush only DHCP (dynamic) addresses from interface, preserving static addresses
#
#  When dhclient renews/rebinds a lease, it calls:
#      ip -4 addr flush dev <interface>
#  This removes ALL IPv4 addresses, including static addresses configured
#  via VyOS (e.g., "set interfaces ethernet eth0 address 192.168.1.1/24").
#
#  The ip() wrapper below intercepts the "ip -4 addr flush" command and replaces
#  it with a selective flush that only removes addresses marked as "dynamic" by
#  the kernel. DHCP-assigned addresses have the "dynamic" flag set automatically.
_flush_dhcp_addrs() {
    local dev="${1:-$interface}"

    logmsg info "Selectively flushing only dynamic (DHCP) addresses from ${dev}"

    local addrs
    addrs=$(/usr/sbin/ip -4 -j addr show dev "${dev}" 2>&1 | jq -r '
      .[] | .addr_info[]? | select(.family == "inet" and .dynamic == true)
      | "\(.local)/\(.prefixlen)"
    ' 2>&1)
    if [ $? -ne 0 ]; then
        logmsg warn "Failed to list dynamic IPv4 addresses on ${dev}; skipping selective flush. ip/jq error output: $addrs"
        return 0
    fi

    while IFS= read -r addr; do
        if [ -n "$addr" ]; then
            logmsg info "Removing dynamic address ${addr} from ${dev}"
            if ! /usr/sbin/ip -4 addr del "${addr}" dev "${dev}" 2>&1; then
                logmsg warn "Failed to remove dynamic address ${addr} from ${dev}"
            fi
        fi
    done <<< "$addrs"

    return 0
}

# get status of FRR
function frr_alive () {
    /usr/lib/frr/watchfrr.sh all_status
    if [ "$?" -eq "0" ] ; then
        logmsg info "FRR status: running"
        return 0
    else
        logmsg info "FRR status: not running"
        return 1
    fi
}

# convert ip route command to vtysh
function iptovtysh () {
    # prepare variables for vtysh command
    local VTYSH_ACTION=$3
    local VTYSH_NETADDR=""
    local VTYSH_GATEWAY=""
    local VTYSH_DEV=""
    local VTYSH_TAG="210"
    local VTYSH_DISTANCE=$IF_METRIC
    # convert default route to 0.0.0.0/0
    if [ "$4" == "default" ] ; then
        VTYSH_NETADDR="0.0.0.0/0"
    else
        VTYSH_NETADDR=$4
    fi
    # add /32 to ip addresses without netmasks
    if [[ ! $VTYSH_NETADDR =~ ^.*/[[:digit:]]+$ ]] ; then
        VTYSH_NETADDR="$VTYSH_NETADDR/32"
    fi
    shift 4
    # get gateway address
    if [ "$1" == "via" ] ; then
        VTYSH_GATEWAY=$2
        shift 2
    fi
    # get device name
    if [ "$1" == "dev" ]; then
        VTYSH_DEV=$2
        shift 2
    fi
    # get distance
    if [ "$1" == "metric" ]; then
        VTYSH_DISTANCE=$2
        shift 2
    fi

    VTYSH_CMD="ip route $VTYSH_NETADDR $VTYSH_GATEWAY $VTYSH_DEV tag $VTYSH_TAG $VTYSH_DISTANCE $VRF_OPTION"

    # delete route if the command is "del"
    if [ "$VTYSH_ACTION" == "del" ] ; then
        VTYSH_CMD="no $VTYSH_CMD"
    fi
    logmsg info "Converted vtysh command: \"$VTYSH_CMD\""
}

# delete the same route from kernel before adding new one
function delroute () {
    logmsg info "Checking if the route presented in kernel: $@ $VRF_OPTION"
    if /usr/sbin/ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then
        logmsg info "Deleting IP route: \"/usr/sbin/ip route del $@ $VRF_OPTION\""
        /usr/sbin/ip route del $@ $VRF_OPTION
    fi
}

# try to communicate with vtysh
function vtysh_conf () {
    # perform 10 attempts with 1 second delay for retries
    for i in {1..10} ; do
        if vtysh  -c "conf t" -c "$1" ; then
            logmsg info "Command was executed successfully via vtysh: \"$1\""
            return 0
        else
            logmsg info "Failed to send command to vtysh, retrying in 1 second"
            sleep 1
        fi
    done
    logmsg error "Failed to execute command via vtysh after 10 attempts: \"$1\""
    return 1
}

# replace ip command with this wrapper
function ip () {
    # Intercept: ip -4 addr flush dev <interface>
    # to preserve static addresses (only remove dynamic/DHCP addresses)
    if [ "$1" = "-4" ] && [ "$2" = "addr" ] && [ "$3" = "flush" ]; then
        logmsg info "Intercepting 'ip -4 addr flush' to preserve static addresses"
        shift 3
        local dev=""
        while [ $# -gt 0 ]; do
            [ "$1" = "dev" ] && dev="$2" && break
            shift
        done
        _flush_dhcp_addrs "${dev:-$interface}"
        return $?
    fi

    # pass command to system `ip` if this is not related to routes change
    if [ "$2" != "route" ] ; then
        logmsg info "Passing command to /usr/sbin/ip: \"$@\""
        /usr/sbin/ip $@
    else
        # if we want to work with routes, try to use FRR first
        if frr_alive ; then
            delroute ${@:4}
            iptovtysh $@
            logmsg info "Sending command to vtysh"
            vtysh_conf "$VTYSH_CMD"
        else
            # add ip route to kernel
            logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\""
            /usr/sbin/ip $@ $VRF_OPTION
        fi
    fi
}