Skip to content

Commit b01a6b3

Browse files
authored
Merge pull request #2619 from unraid/codex/docker-endpoint-mac-address
fix(docker): set fixed MACs on network endpoints
2 parents f8e4427 + ba55802 commit b01a6b3

4 files changed

Lines changed: 170 additions & 24 deletions

File tree

emhttp/languages/en_US/helptext.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2379,6 +2379,12 @@ Generally speaking, it is recommended to leave this setting to its default value
23792379
IMPORTANT NOTE: If adjusting port mappings, do not modify the settings for the Container port as only the Host port can be adjusted.
23802380
:end
23812381

2382+
:docker_fixed_mac_help:
2383+
Assigns the container's MAC address on the selected Docker network endpoint. Use a valid unicast MAC address; the first octet must be even, e.g. 02:42:9a:0d:7e:c0.
2384+
2385+
This avoids using the legacy container-level --mac-address option in Extra Parameters.
2386+
:end
2387+
23822388
:docker_container_network_help:
23832389
This allows your container to utilize the network configuration of another container. Select the appropriate container from the list.<br>This setup can be particularly beneficial if you wish to route your container's traffic through a VPN.
23842390
:end

emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,18 +202,17 @@ function cpu_pinning() {
202202
if (preg_match('/^container:(.*)/', $Network)) {
203203
$Net_Container = str_replace("container:", "", $Network);
204204
} else {
205-
preg_match("/--(net|network)=container:[^\s]+/", $ExtraParams, $NetworkParam);
206-
if (!empty($NetworkParam[0])) {
207-
$Net_Container = explode(':', $NetworkParam[0])[1];
208-
$Net_Container = str_replace(['"', "'"], '', $Net_Container);
205+
preg_match("/--(?:net|network)(?:=|\s+)(['\"]?)container:([^'\"\s]+)\\1/", $ExtraParams, $NetworkParam);
206+
if (!empty($NetworkParam[2])) {
207+
$Net_Container = $NetworkParam[2];
209208
}
210209
}
211210
// check if the container still exists from which the network should be used, if it doesn't exist any more recreate container with network none and don't start it
212211
if (!empty($Net_Container)) {
213212
$Net_Container_ID = $DockerClient->getContainerID($Net_Container);
214213
if (empty($Net_Container_ID)) {
215214
$cmd = str_replace('/docker run -d ', '/docker create ', $cmd);
216-
$cmd = preg_replace("/--(net|network)=(['\"]?)container:[^'\"]+\\2/", "--network=none ", $cmd);
215+
$cmd = preg_replace("/--(?:net|network)(?:=|\s+)(['\"]?)container:[^'\"\s]+\\1/", "--network=none ", $cmd);
217216
}
218217
}
219218
// force kill container if still running after time-out
@@ -736,6 +735,8 @@ function removeConfig(num) {
736735

737736
function prepareConfig(form) {
738737
var types = [], values = [], targets = [], vcpu = [];
738+
var myMAC = $(form).find('input[name="contMyMAC"]').val().trim().replaceAll('-', ':').toLowerCase();
739+
$(form).find('input[name="contMyMAC"]').val(myMAC);
739740
if ($('select[name="contNetwork"]').val()=='host') {
740741
$(form).find('input[name="confType[]"]').each(function(){types.push($(this).val());});
741742
$(form).find('input[name="confValue[]"]').each(function(){values.push($(this));});
@@ -744,6 +745,7 @@ function prepareConfig(form) {
744745
}
745746
$(form).find('input[id^="box"]').each(function(){if ($(this).prop('checked')) vcpu.push($('#'+$(this).prop('id').replace('box','cpu')).text());});
746747
form.contCPUset.value = vcpu.join(',');
748+
return true;
747749
}
748750

749751
function makeName(type) {
@@ -893,7 +895,7 @@ function prepareCategory() {
893895
?>
894896

895897
<div id="canvas">
896-
<form markdown="1" method="POST" autocomplete="off" onsubmit="prepareConfig(this)">
898+
<form markdown="1" method="POST" autocomplete="off" onsubmit="return prepareConfig(this)">
897899
<input type="hidden" name="csrf_token" value="<?=$var['csrf_token']?>">
898900
<input type="hidden" name="contCPUset" value="">
899901
<?if ($xmlType=='edit'):?>
@@ -1111,6 +1113,14 @@ function prepareCategory() {
11111113

11121114
</div>
11131115

1116+
<div markdown="1" class="myMAC noshow">
1117+
_(Fixed MAC address)_ (_(optional)_):
1118+
: <input type="text" name="contMyMAC" pattern="([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}|[0-9A-Fa-f]{12}">
1119+
1120+
:docker_fixed_mac_help:
1121+
1122+
</div>
1123+
11141124
<div markdown="1" class="netCONT noshow">
11151125
_(Container Network)_:
11161126
: <select name="netCONT" id="netCONT">
@@ -1560,19 +1570,30 @@ function prepareCategory() {
15601570
<?endforeach;?>
15611571

15621572
function showSubnet(bridge) {
1563-
if (bridge.match(/^(bridge|host|none)$/i) !== null) {
1573+
if (bridge.match(/^(host|none)$/i) !== null) {
15641574
$('.myIP').hide();
15651575
$('input[name="contMyIP"]').val('');
1576+
$('.myMAC').hide();
1577+
$('input[name="contMyMAC"]').val('');
1578+
$('.netCONT').hide();
1579+
$('#netCONT').val('');
1580+
} else if (bridge.match(/^(bridge)$/i) !== null) {
1581+
$('.myIP').hide();
1582+
$('input[name="contMyIP"]').val('');
1583+
$('.myMAC').show();
15661584
$('.netCONT').hide();
15671585
$('#netCONT').val('');
15681586
} else if (bridge.match(/^(container)$/i) !== null) {
15691587
$('.netCONT').show();
15701588
$('#netCONT').val('<?php echo (isset($xml) && isset($xml['Network'][1])) ? $xml['Network'][1] : ''; ?>');
15711589
$('.myIP').hide();
15721590
$('input[name="contMyIP"]').val('');
1591+
$('.myMAC').hide();
1592+
$('input[name="contMyMAC"]').val('');
15731593
} else {
15741594
$('.myIP').show();
15751595
$('#myIP').html('<?=_('Subnet')?>: '+subnet[bridge]);
1596+
$('.myMAC').show();
15761597
$('.netCONT').hide();
15771598
$('#netCONT').val('');
15781599
}
@@ -1932,4 +1953,3 @@ function load_contOverview() {
19321953
}
19331954
</script>
19341955
<?END:?>
1935-

emhttp/plugins/dynamix.docker.manager/include/Helpers.php

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,66 @@ function xml_decode($string) {
3232
return strval(html_entity_decode($string, ENT_XML1, 'UTF-8'));
3333
}
3434

35+
function extraParamsWithQuotedValuesMasked($extraParams) {
36+
return preg_replace('/"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\']*\'/', '""', $extraParams);
37+
}
38+
39+
function replaceUnquotedExtraParams($extraParams, $callback) {
40+
$parts = preg_split('/("[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\']*\')/', $extraParams, -1, PREG_SPLIT_DELIM_CAPTURE);
41+
if ($parts === false) {
42+
return $extraParams;
43+
}
44+
foreach ($parts as $i => $part) {
45+
if ($part === '' || $part[0] === '"' || $part[0] === "'") {
46+
continue;
47+
}
48+
$parts[$i] = $callback($part);
49+
}
50+
return implode('', $parts);
51+
}
52+
53+
function extractMacAddressParam($extraParams) {
54+
if (!is_string($extraParams)) {
55+
return '';
56+
}
57+
$extraParams = extraParamsWithQuotedValuesMasked($extraParams);
58+
if (preg_match('/(?:^|\s)--mac-address=([^\s\'"]+)/', $extraParams, $match)) {
59+
return trim($match[1]);
60+
}
61+
if (preg_match('/(?:^|\s)--mac-address\s+([^\s\'"]+)/', $extraParams, $match)) {
62+
return trim($match[1]);
63+
}
64+
return '';
65+
}
66+
67+
function removeMacAddressParam($extraParams) {
68+
if (!is_string($extraParams) || $extraParams === '') {
69+
return '';
70+
}
71+
$extraParams = replaceUnquotedExtraParams($extraParams, function($part) {
72+
$part = preg_replace('/(^|\s)--mac-address=[^\s\'"]+/', '$1', $part);
73+
return preg_replace('/(^|\s)--mac-address\s+[^\s\'"]+/', '$1', $part);
74+
});
75+
return trim($extraParams);
76+
}
77+
78+
function hasNetworkParam($extraParams) {
79+
return is_string($extraParams) && preg_match('/(?:^|\s)--net(?:work)?(?:=|\s+)[^\s\'"]+/', extraParamsWithQuotedValuesMasked($extraParams));
80+
}
81+
82+
function normalizeMacAddress($mac) {
83+
$mac = strtolower(trim($mac ?? ''));
84+
if ($mac === '') {
85+
return '';
86+
}
87+
if (preg_match('/^[0-9a-f]{12}$/', $mac)) {
88+
$mac = implode(':', str_split($mac, 2));
89+
} else {
90+
$mac = str_replace('-', ':', $mac);
91+
}
92+
return preg_match('/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/', $mac) ? $mac : '';
93+
}
94+
3595
function generateTSwebui($url, $serve, $webUI) {
3696
if (!isset($webUI)) {
3797
return '';
@@ -75,6 +135,9 @@ function postToXML($post, $setOwnership=false) {
75135
$xml->Network = xml_encode($post['contNetwork']);
76136
}
77137
$xml->MyIP = xml_encode($post['contMyIP']);
138+
$extraNetwork = hasNetworkParam($post['contExtraParams'] ?? '');
139+
$myMAC = $extraNetwork ? '' : normalizeMacAddress(trim($post['contMyMAC'] ?? '') ?: extractMacAddressParam($post['contExtraParams'] ?? ''));
140+
$xml->MyMAC = xml_encode($myMAC);
78141
$xml->Shell = xml_encode($post['contShell']);
79142
$xml->Privileged = strtolower($post['contPrivileged']??'')=='on' ? 'true' : 'false';
80143
$xml->Support = xml_encode($post['contSupport']);
@@ -85,7 +148,7 @@ function postToXML($post, $setOwnership=false) {
85148
$xml->WebUI = xml_encode(trim($post['contWebUI']));
86149
$xml->TemplateURL = xml_encode($post['contTemplateURL']);
87150
$xml->Icon = xml_encode(trim($post['contIcon']));
88-
$xml->ExtraParams = xml_encode($post['contExtraParams']);
151+
$xml->ExtraParams = xml_encode($myMAC && !$extraNetwork ? removeMacAddressParam($post['contExtraParams']) : $post['contExtraParams']);
89152
$xml->PostArgs = xml_encode($post['contPostArgs']);
90153
$xml->CPUset = xml_encode($post['contCPUset']);
91154
$xml->DateInstalled = xml_encode(time());
@@ -149,6 +212,9 @@ function xmlToVar($xml) {
149212
$out['Registry'] = xml_decode($xml->Registry);
150213
$out['Network'] = xml_decode($xml->Network);
151214
$out['MyIP'] = xml_decode($xml->MyIP ?? '');
215+
$extraParams = xml_decode($xml->ExtraParams ?? '');
216+
$extraNetwork = hasNetworkParam($extraParams);
217+
$out['MyMAC'] = $extraNetwork ? '' : normalizeMacAddress(xml_decode($xml->MyMAC ?? '') ?: extractMacAddressParam($extraParams));
152218
$out['Shell'] = xml_decode($xml->Shell ?? 'sh');
153219
$out['Privileged'] = xml_decode($xml->Privileged);
154220
$out['Support'] = xml_decode($xml->Support);
@@ -159,7 +225,7 @@ function xmlToVar($xml) {
159225
$out['WebUI'] = xml_decode($xml->WebUI);
160226
$out['TemplateURL'] = xml_decode($xml->TemplateURL);
161227
$out['Icon'] = xml_decode($xml->Icon);
162-
$out['ExtraParams'] = xml_decode($xml->ExtraParams);
228+
$out['ExtraParams'] = $extraParams;
163229
$out['PostArgs'] = xml_decode($xml->PostArgs);
164230
$out['CPUset'] = xml_decode($xml->CPUset);
165231
$out['DonateText'] = xml_decode($xml->DonateText);
@@ -325,13 +391,29 @@ function xmlToCommand($xml, $create_paths=false) {
325391
$xml = xmlToVar($xml);
326392
$cmdName = strlen($xml['Name']) ? '--name='.escapeshellarg($xml['Name']) : '';
327393
$cmdPrivileged = strtolower($xml['Privileged'])=='true' ? '--privileged=true' : '';
394+
$extraNetwork = hasNetworkParam($xml['ExtraParams']);
395+
$cmdMyIP = '';
328396
if (preg_match('/^container:(.*)/', $xml['Network'])) {
329-
$cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg($xml['Network']);
397+
$cmdNetwork = $extraNetwork ? "" : '--net='.escapeshellarg($xml['Network']);
330398
} else {
331-
$cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg(strtolower($xml['Network']));
399+
$networkName = strtolower($xml['Network']);
400+
if ($extraNetwork) {
401+
$cmdNetwork = "";
402+
} elseif (strlen($xml['MyMAC']) && !in_array($networkName, ['host','none'])) {
403+
$xml['ExtraParams'] = removeMacAddressParam($xml['ExtraParams']);
404+
$networkEndpoint = ['name='.$networkName];
405+
foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) {
406+
if ($myIP) $networkEndpoint[] = (strpos($myIP,':') !== false ? 'ip6=' : 'ip=').$myIP;
407+
}
408+
$networkEndpoint[] = 'mac-address='.$xml['MyMAC'];
409+
$cmdNetwork = '--network='.escapeshellarg(implode(',', $networkEndpoint));
410+
} else {
411+
$cmdNetwork = '--net='.escapeshellarg($networkName);
412+
}
413+
}
414+
if (!strlen($xml['MyMAC']) || preg_match('/^container:(.*)/', $xml['Network']) || $extraNetwork) {
415+
foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) if ($myIP) $cmdMyIP .= (strpos($myIP,':') !== false ? '--ip6=' : '--ip=').escapeshellarg($myIP).' ';
332416
}
333-
$cmdMyIP = '';
334-
foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) if ($myIP) $cmdMyIP .= (strpos($myIP,':')?'--ip6=':'--ip=').escapeshellarg($myIP).' ';
335417
$cmdCPUset = strlen($xml['CPUset']) ? '--cpuset-cpus='.escapeshellarg($xml['CPUset']) : '';
336418
$Volumes = [''];
337419
$Ports = [''];

etc/rc.d/rc.docker

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,16 @@ netrestore_connect(){
245245
local MY_TT=$3
246246
local MY_MAC=$4
247247
local MY_IP=
248-
local MY_OPTS=
248+
local MY_IPV4=
249+
local MY_IPV6=
249250
local IP=
251+
local IPAM_JSON=
252+
local ENDPOINT_JSON=
253+
local CONNECT_JSON=
254+
local CODE=
255+
local BODY=
250256
local ENDPOINT_ID=
257+
local ENDPOINT_MAC=
251258
local OUT=
252259

253260
container_exist "$CONTAINER" || return 0
@@ -263,23 +270,49 @@ netrestore_connect(){
263270
return 1
264271
fi
265272

266-
ENDPOINT_ID=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.EndpointID}}{{end}}" "$CONTAINER" 2>/dev/null)
267-
[[ -n $ENDPOINT_ID ]] && return 0
268-
269273
for IP in ${MY_TT//;/ }; do
270274
[[ -n $IP ]] || continue
271275
if [[ $IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
276+
MY_IPV4=$IP
272277
MY_IP="$MY_IP --ip $IP"
273278
elif [[ $IP =~ : ]]; then
279+
MY_IPV6=$IP
274280
MY_IP="$MY_IP --ip6 $IP"
275281
else
276282
log "skipping invalid stored IP for $CONTAINER on network $NETWORK: $IP"
277283
fi
278284
done
279285

280-
[[ -n $MY_MAC ]] && MY_OPTS="--driver-opt=com.docker.network.endpoint.macaddress=$MY_MAC"
286+
ENDPOINT_ID=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.EndpointID}}{{end}}" "$CONTAINER" 2>/dev/null)
287+
if [[ -n $ENDPOINT_ID ]]; then
288+
[[ -n $MY_MAC ]] || return 0
289+
ENDPOINT_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.MacAddress}}{{end}}" "$CONTAINER" 2>/dev/null)
290+
[[ ${ENDPOINT_MAC,,} == ${MY_MAC,,} ]] && return 0
291+
log "reconnecting $CONTAINER to network $NETWORK to restore MAC $MY_MAC"
292+
if ! OUT=$(docker network disconnect -f "$NETWORK" "$CONTAINER" 2>&1); then
293+
log "failed to disconnect $CONTAINER from network $NETWORK: $OUT"
294+
return 1
295+
fi
296+
fi
297+
298+
if [[ -n $MY_MAC ]]; then
299+
[[ -n $MY_IPV4 ]] && IPAM_JSON="\"IPv4Address\":\"$MY_IPV4\""
300+
[[ -n $MY_IPV6 ]] && IPAM_JSON="${IPAM_JSON:+$IPAM_JSON,}\"IPv6Address\":\"$MY_IPV6\""
301+
ENDPOINT_JSON="\"MacAddress\":\"$MY_MAC\""
302+
[[ -n $IPAM_JSON ]] && ENDPOINT_JSON="\"IPAMConfig\":{$IPAM_JSON},$ENDPOINT_JSON"
303+
CONNECT_JSON="{\"Container\":\"$CONTAINER\",\"EndpointConfig\":{$ENDPOINT_JSON}}"
304+
OUT=$(curl --unix-socket /var/run/docker.sock -sS -w $'\n%{http_code}' -X POST -H "Content-Type: application/json" --data "$CONNECT_JSON" "http://localhost/networks/$NETWORK/connect" 2>&1)
305+
CODE=${OUT##*$'\n'}
306+
BODY=${OUT%$'\n'$CODE}
307+
if [[ $CODE != 2* ]]; then
308+
log "failed to connect $CONTAINER to network $NETWORK: $BODY"
309+
return 1
310+
fi
311+
return 0
312+
fi
313+
281314
log "connecting $CONTAINER to network $NETWORK"
282-
if ! OUT=$(docker network connect $MY_OPTS $MY_IP $NETWORK $CONTAINER 2>&1); then
315+
if ! OUT=$(docker network connect $MY_IP $NETWORK $CONTAINER 2>&1); then
283316
log "failed to connect $CONTAINER to network $NETWORK: $OUT"
284317
return 1
285318
fi
@@ -347,20 +380,25 @@ docker_network_start(){
347380
REBUILD=1
348381
fi
349382
done
350-
MY_NETWORK= MY_IP= MY_MAC= TEMPLATE_MAC= CUSTOM_PRIMARY=
383+
MY_NETWORK= MY_IP= MY_MAC= XML_MAC= TEMPLATE_MAC= CUSTOM_PRIMARY=
351384
while read_dom; do
352385
[[ $ENTITY == Network ]] && MY_NETWORK=$CONTENT
353386
[[ $ENTITY == MyIP ]] && MY_IP=${CONTENT// /,} && MY_IP=$(echo "$MY_IP" | tr -s "," ";")
387+
[[ $ENTITY == MyMAC ]] && XML_MAC=${CONTENT// /}
354388
done <$XMLFILE
355389
# only restore valid networks
356390
if [[ -n $MY_NETWORK ]]; then
357391
[[ $MY_NETWORK =~ ^(br|bond|eth|wlan)[0-9]+(\.[0-9]+)?$ ]] && CUSTOM_PRIMARY=1
358392
TEMPLATE_MAC=$(sed -nE 's@.*<ExtraParams>.*--mac-address(=|[[:space:]]+)([^ <]+).*@\2@p' "$XMLFILE" | head -n1)
359-
MY_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$MY_NETWORK\"}}{{.MacAddress}}{{end}}" $CONTAINER 2>/dev/null)
360-
[[ -n $MY_MAC ]] || MY_MAC=$TEMPLATE_MAC
393+
if [[ -n $XML_MAC ]]; then
394+
MY_MAC=$XML_MAC
395+
else
396+
MY_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$MY_NETWORK\"}}{{.MacAddress}}{{end}}" $CONTAINER 2>/dev/null)
397+
[[ -n $MY_MAC ]] || MY_MAC=$TEMPLATE_MAC
398+
fi
361399
netrestore_add "$MY_NETWORK" "$CONTAINER" "$MY_IP" "$MY_MAC"
362400
PRIMARY_NETWORK[$CONTAINER]=$MY_NETWORK
363-
[[ -n $REBUILD || (-n $TEMPLATE_MAC && -n $CUSTOM_PRIMARY) ]] && REBUILD_CONTAINERS[$CONTAINER]=1
401+
[[ -n $REBUILD || (-z $XML_MAC && -n $TEMPLATE_MAC && -n $CUSTOM_PRIMARY) ]] && REBUILD_CONTAINERS[$CONTAINER]=1
364402
fi
365403
fi
366404
# restore user defined networks

0 commit comments

Comments
 (0)