Random mac address

NEWMAC0=$(dd if=/dev/urandom bs=1024 count=1 2>/dev/null | md5sum | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)..$/\1\2\3\4\5\6/')
NEWMAC=$(echo ${NEWMAC0} | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)..
$/32:\2:\3:\4:\5:81/')
uci set network..lan_dev..macaddr=${NEWMAC}
NEWMAC=$(echo ${NEWMAC0} | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)..$/32:\2:\3:\4:\5:82/')
uci set network..wan_dev..macaddr=${NEWMAC}
NEWMAC=$(echo ${NEWMAC0} | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)..
$/32:\2:\3:\4:\5:83/')
uci set wireless..@wifi-iface[0]..macaddr=${NEWMAC}
NEWMAC=$(echo ${NEWMAC0} | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)..$/32:\2:\3:\4:\5:84/')
uci set wireless..@wifi-iface[1]..macaddr=${NEWMAC}
uci set network..wwan..hostname=$(dd if=/dev/urandom bs=1024 count=1 2>/dev/null | md5sum | sed 's/^\(..\)\(..\)\(..\)..
$/\1\2\3/')
uci commit network
uci commit wireless
NEWMAC=$(echo ${NEWMAC0} | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\)..*$/32:\2:\3:\4:\5:80/')
ifconfig eth0 down;ifconfig eth0 hw ether ${NEWMAC};ifconfig eth0 up
/etc/init..d/network restart

One problem...use double quotes instead of single quotes.

This works...

dd if=/dev/urandom bs=1024 count=1 2>/dev/null | md5sum | sed "s/^(…)(…)(…)(…)(…)(…).$/\1\2\3\4\5\6/"

You probably should consider using the convention for "LAA" (locally administered address) MAC addresses. Second-least-significant bit of the first (most-significant) octet is set. Probably also should make sure the LSB of that octet is 0, indicating a unicast address.

Also, printf "%02x:%02x:%02x:%02x:%02x:%02x" $b1 $b2 $b3 $b4 $b5 $b6 is probably a lot faster and, at least for me, more understandable than using md5sum to convert from a number to a hex string, then the mess of sed to insert colons. This also lets you easily set the first byte (or the first three, for that matter -- they're the manufacturer ID) to indicate an LAA.

#!/usr/bin/env python3
"""
This script is meant to persuade the ISP to issue a new IP address for the
WAN side of the router.  It changes the router's WAN MAC address and then
reboots the router with a built-in 30-second delay.  The new MAC address is
random except that the "locally administered" bit is set (assuming I have
correctly understood which bit that is).

Experience with very few ISPs shows that this often works, but it does
not *always* work.  We run it twice, once to get a new IP address, and
after rebooting, again to be sure that got a new IP address. 

READ THIS FIRST:

(0) Same open-source license as OpenWRT 18.06.2.  Use at your own risk, etc.
    Do not assume that it works.  Test carefully at least twice.

(1) Depends on the python3-light package.

(2) Tested successfully with OpenWRT 18.06.2 on TPLink Archer C7 AC1750 v4
    and v5.

(3) Doesn't work on OpenWRT 18.06.2 on TPLink Archer C7 AC1750 v2.  Evidently
    "option macaddr ..." doesn't work in that context.

(4) These files will be tweaked:

        ## /overlay/upper/etc/config/network wherein
                "option macaddr ..." is already specified
                inside a "config interface wan" stanza or
                a "config device wan_dev" stanza.

        ## /etc/rc.d/S10boot

        ## /etc/rc.d/S95done

(5) When testing, begin by copying those 3 files to a safe place, and end
    by comparing them to their tweaked versions.

#######################
## Sample run begins ##
#######################

root@myComputer# ssh OpenWRT
BusyBox v1.28.4 () built-in shell (ash)
OpenWrt 18.06.2, r7676-cddd7b4c77
@OpenWRT:/root# getNewIpAddress


This will stop the network for more than a minute, so
DON'T DO THIS IF ANYONE IS USING A VOIP PHONE, for example.

The MAC address of this router will be changed from
33:91:f4:7f:ab:46 to 4f:a9:dd:76:eb:ad, the router will be rebooted, and
presumably the ISP will issue it an IP address other than
its present IP address (xxx.xxx.217.161).


Do it (yes/no)? > yes
@OpenWRT:/root# Connection to OpenWRT closed by remote host.
Connection to OpenWRT closed.

[...wait for reboot...]

root@myComputer# ssh OpenWRT
BusyBox v1.28.4 () built-in shell (ash)
OpenWrt 18.06.2, r7676-cddd7b4c77

@OpenWRT:/root# getNewIpAddress


This will stop the network for more than a minute, so
DON'T DO THIS IF ANYONE IS USING A VOIP PHONE, for example.

The MAC address of this router's WAN interface will be changed from
4f:a9:dd:76:eb:ad to 3b:a5:96:9f:9b:af, the router will be rebooted, and
presumably the ISP will issue it an IP address other than
its present IP address (xxx.xxx.219.148).


Do it (yes/no)? > no
aborted.
@OpenWRT:/root#

#####################
## Sample run ends ##
#####################

Steve Newcomb 20190514
s r n at@at c o o l h e a d s d o t c o m
"""

import os, random, re, subprocess, sys

S10bootFilePath = '/etc/rc.d/S10boot'
S10bootRE = re.compile(
    ''.join(
        [
            r'(?<![^\r\n])',
            r'boot\(\)[ \t]*\{[ \t]*[\r\n]+',
            r'(',
                r'[ \t]*',
                r'sleep [0-9]+',
                r'[^\r\n]*[\r\n]+',
            r'|',
                r'[ \t]*[\r\n]+',  ## blank line holding characters open
            r')?',
        ],
    ),
)

S95doneFilePath = '/etc/rc.d/S95done'
S95doneRE = re.compile(
    ''.join(
        [
            r'(?<![^\r\n])',
            r'boot\(\)[ \t]*\{[ \t]*[\r\n]+',
            r'(',
                r'[ \t]*',
                r'/usr/bin/getNewIpAddress',
                r'[^\r\n]*[\r\n]+',
            r'|',
                r'[ \t]*[\r\n]+',  ## blank line holding characters open
            r')?',
        ],
    ),
)

###############################################################
def restoreboot( S10bootLine, S95doneLine):
    global _S95doneLine
    _S95doneLine = S95doneLine
    S10bootFO = open( S10bootFilePath, 'r', encoding = 'utf-8')
    S10bootText = S10bootFO.read()
    S10bootFO.close()
    S10bootFO = open( S10bootFilePath, 'w', encoding = 'utf-8')
    S10bootFO.write(
        S10bootRE.sub(
            S10bootLine,
            S10bootText,
        ),
    )    
    S10bootFO.close()

    if S95doneLine is not None:
        S95doneFO = open( S95doneFilePath, 'r', encoding = 'utf-8')
        S95doneText = S95doneFO.read()
        S95doneFO.close()
        S95doneFO = open( S95doneFilePath, 'w', encoding = 'utf-8')
        S95doneFO = open( S95doneFilePath, 'w', encoding = 'utf-8')
        def S95donereplaceLineWithSpaceHolding( MO):
            global _S95doneLine
            charCnt = max(
                len( MO.group( 0)),
                len( _S95doneLine),
            )
            formatStr = '%-charCnt.charCnts\n'.replace(
                'charCnt',
                str( charCnt),
            )
            return formatStr % ( _S95doneLine)
        S95doneFO.write(
            S95doneRE.sub(
                S95donereplaceLineWithSpaceHolding,
                S95doneText,
            ),
        )    
        S95doneFO.close()
###############################################################
    
        
## ###
## print( sys.argv)
## ###
if len( sys.argv) == 1:
    pass
elif len( sys.argv) == 2 and sys.argv[ 1] == 'restoreboot':
    restoreboot( 'boot() {\n', None,)
    sys.exit()
else:
    print( 'unrecognized arg(s): %r' % ( sys.argv[ 1:]))
    sys.exit( 1)

## newMAC=` cat /dev/urandom | od -An -vtx1 -N6 | sed -e s/\ // -e s/\ /:/g `
randBytesInHex = []
randInt = random.randint( (2**24) + 1, ( 2**57) - 1)
for byteCtr in range( 6):
    randInt >>= 8
    randBytesInHex.append(
        '%02x' % (
            (
                ( randInt & 0xFE)
                |
                0x02 ## this is a "locally-administered" MAC address (https://en.wikipedia.org/wiki/MAC_address#Universal_vs._local)
            ) if ( byteCtr == 5) else (
                ( randInt & 0xFF)
            ),
        ),
    )
randBytesInHex.reverse()
newMAC = ':'.join( randBytesInHex)

networkConfFilePath = '/overlay/upper/etc/config/network'
networkConfFO = open( networkConfFilePath, 'r', encoding = 'utf-8')
networkConfText = networkConfFO.read()
networkConfFO.close()
oldMACRE = re.compile(
    ''.join(
        [
            r'(?<![^\r\n])',
            r'[ \t]+',
            r'option macaddr \'?',
            r'(?P<macaddr>',
                r'[0-9a-f:]{17}'
            r')',
        ],
    ),
)
try:
## ###
##     print( networkConfText)
## ###
    stanzaContainingOldMAC = re.compile(
        ''.join(
            [
                r'(?<![^\r\n])',
                r'config device \'wan_dev\'[\r\n]+',
                r'(',
                    r'[ \t]+',
                    r'option ',
                    r'[^\r\n]+'
                    r'[\r\n]+',
                r')*',
            ],
        ),
    ).search( networkConfText).group( 0)
## ###
##     print( stanzaContainingOldMAC)
## ###
    oldMAC = oldMACRE.search( stanzaContainingOldMAC).group( 'macaddr')
except ( AttributeError, TypeError):
    stanzaContainingOldMAC = re.compile(
        ''.join(
            [
                r'(?<![^\r\n])',
                r'config interface \'wan\'[\r\n]+',
                r'(',
                    r'[ \t]+',
                    r'option ',
                    r'[^\r\n]+'
                    r'[\r\n]+',
                r')*',
            ],
        ),
    ).search( networkConfText).group( 0)
## ###
##     print( stanzaContainingOldMAC)
## ###
    try:
        oldMAC = oldMACRE.search( stanzaContainingOldMAC).group( 'macaddr')
    except ( AttributeError, TypeError):
        print( 
            '''Error: In %r the stanza

%s

contains no 

    option macaddr \'01:02:03:04:05:06\'

''' % ( 
                networkConfFilePath,
                stanzaContainingOldMAC,
            ),
        )
        sys.exit( 1)
## set -- `cat /etc/config/network | egrep 'macaddr\ ' | sed $'s/\'//g' `
## oldMAC=$3



subProc = subprocess.Popen(
    [
        'ifconfig',
        'eth0.2',
    ],
    stdin = subprocess.PIPE,
    stdout = subprocess.PIPE,
    stderr = subprocess.PIPE,
)
(
    stdMsg,
    errMsg,
) = subProc.communicate()
stdMsg = stdMsg.decode( 'utf-8')
existingIpAddress = re.compile(
    ''.join(
        [
            r'inet addr:',
            r'(?P<existingIpAddress>',
                r'[0-9\.]+',
            r')',
        ],
    ),
).search( stdMsg).group( 'existingIpAddress')
## ###
## print( stdMsg)
## ###
print( '''

This will stop the network for more than a minute, so
DON'T DO THIS IF ANYONE IS USING A VOIP PHONE, for example.

The MAC address of this router's WAN interface will be changed from
oldMAC to newMAC, the router will be rebooted, and
presumably the ISP will issue it an IP address other than
its present IP address (existingIpAddress).

'''.replace(
        'newMAC',
        newMAC,
    ).replace(
        'oldMAC',
        oldMAC,
    ).replace(
        'existingIpAddress',
        existingIpAddress,
    ),
)

while True:
    yesOrNo = input( "Do it (yes/no)? > ")
    if yesOrNo in [ 'yes', 'no',]:
        break
if yesOrNo != 'yes':
    print( 'aborted.')
    sys.exit()

stanzaContainingNewMAC = stanzaContainingOldMAC.replace(
    oldMAC,
    newMAC,
)

newNetworkConfText = networkConfText.replace(
    stanzaContainingOldMAC,
    stanzaContainingNewMAC,
)

networkConfFO = open( networkConfFilePath, 'w', encoding = 'utf-8')
networkConfFO.write( newNetworkConfText)
networkConfFO.close()

restoreboot( 'boot() {\n\tsleep 30\n', 'boot() {\n\t/usr/bin/getNewIpAddress restoreboot\n',)

os.system( 'reboot')

Bit 1 (OR with 0x02) does indicates a locally generated MAC but bit 0 must never be set on the first byte of a MAC address.

As Python is a rather large package, MAC address manipulation can also be done with the existing sh functions available in /lib/

See, for example:

/lib/functions/system.sh:macaddr_add()
/lib/functions/system.sh:macaddr_setbit_la()
/lib/functions/system.sh:macaddr_2bin()
/lib/functions/system.sh:macaddr_canonicalize()

hexdump of /dev/random can get you random bytes in a format of your choosing.

1 Like

Fixed. Leftmost octet mask 0xFF -> 0xFE.

What you say about the size of Python3 is true enough, but I can't do everything the script does, nor as safely as it does it, using these functions alone. Originally, I wrote an ash script for this purpose, but I could not make it as robust, self-explanatory. or maintainable and retargetable as the Python3 script above. For those reasons, Python3 is worth the memory, at least to me, but it wouldn't be as practical if I were using a snapshot, because I also want luci. (Luci occupies a lot of space if you have to install it in /overlay, and that's evidently necessary in a snapshot.)

Just found a solution here, just in case someone else is still searching: