diff --git a/emhttp/languages/en_US/helptext.txt b/emhttp/languages/en_US/helptext.txt
index e3fcb4a19..5dfac30c2 100644
--- a/emhttp/languages/en_US/helptext.txt
+++ b/emhttp/languages/en_US/helptext.txt
@@ -2379,6 +2379,12 @@ Generally speaking, it is recommended to leave this setting to its default value
IMPORTANT NOTE: If adjusting port mappings, do not modify the settings for the Container port as only the Host port can be adjusted.
:end
+:docker_fixed_mac_help:
+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.
+
+This avoids using the legacy container-level --mac-address option in Extra Parameters.
+:end
+
:docker_container_network_help:
This allows your container to utilize the network configuration of another container. Select the appropriate container from the list. This setup can be particularly beneficial if you wish to route your container's traffic through a VPN.
:end
diff --git a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php
index 794faadfb..2966cf214 100755
--- a/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php
+++ b/emhttp/plugins/dynamix.docker.manager/include/CreateDocker.php
@@ -202,10 +202,9 @@ function cpu_pinning() {
if (preg_match('/^container:(.*)/', $Network)) {
$Net_Container = str_replace("container:", "", $Network);
} else {
- preg_match("/--(net|network)=container:[^\s]+/", $ExtraParams, $NetworkParam);
- if (!empty($NetworkParam[0])) {
- $Net_Container = explode(':', $NetworkParam[0])[1];
- $Net_Container = str_replace(['"', "'"], '', $Net_Container);
+ preg_match("/--(?:net|network)(?:=|\s+)(['\"]?)container:([^'\"\s]+)\\1/", $ExtraParams, $NetworkParam);
+ if (!empty($NetworkParam[2])) {
+ $Net_Container = $NetworkParam[2];
}
}
// 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
@@ -213,7 +212,7 @@ function cpu_pinning() {
$Net_Container_ID = $DockerClient->getContainerID($Net_Container);
if (empty($Net_Container_ID)) {
$cmd = str_replace('/docker run -d ', '/docker create ', $cmd);
- $cmd = preg_replace("/--(net|network)=(['\"]?)container:[^'\"]+\\2/", "--network=none ", $cmd);
+ $cmd = preg_replace("/--(?:net|network)(?:=|\s+)(['\"]?)container:[^'\"\s]+\\1/", "--network=none ", $cmd);
}
}
// force kill container if still running after time-out
@@ -736,6 +735,8 @@ function removeConfig(num) {
function prepareConfig(form) {
var types = [], values = [], targets = [], vcpu = [];
+ var myMAC = $(form).find('input[name="contMyMAC"]').val().trim().replaceAll('-', ':').toLowerCase();
+ $(form).find('input[name="contMyMAC"]').val(myMAC);
if ($('select[name="contNetwork"]').val()=='host') {
$(form).find('input[name="confType[]"]').each(function(){types.push($(this).val());});
$(form).find('input[name="confValue[]"]').each(function(){values.push($(this));});
@@ -744,6 +745,7 @@ function prepareConfig(form) {
}
$(form).find('input[id^="box"]').each(function(){if ($(this).prop('checked')) vcpu.push($('#'+$(this).prop('id').replace('box','cpu')).text());});
form.contCPUset.value = vcpu.join(',');
+ return true;
}
function makeName(type) {
@@ -893,7 +895,7 @@ function prepareCategory() {
?>
-
+
+_(Fixed MAC address)_ (_(optional)_):
+:
+
+:docker_fixed_mac_help:
+
+
+
_(Container Network)_:
:
@@ -1560,9 +1570,17 @@ function prepareCategory() {
function showSubnet(bridge) {
- if (bridge.match(/^(bridge|host|none)$/i) !== null) {
+ if (bridge.match(/^(host|none)$/i) !== null) {
$('.myIP').hide();
$('input[name="contMyIP"]').val('');
+ $('.myMAC').hide();
+ $('input[name="contMyMAC"]').val('');
+ $('.netCONT').hide();
+ $('#netCONT').val('');
+ } else if (bridge.match(/^(bridge)$/i) !== null) {
+ $('.myIP').hide();
+ $('input[name="contMyIP"]').val('');
+ $('.myMAC').show();
$('.netCONT').hide();
$('#netCONT').val('');
} else if (bridge.match(/^(container)$/i) !== null) {
@@ -1570,9 +1588,12 @@ function showSubnet(bridge) {
$('#netCONT').val('');
$('.myIP').hide();
$('input[name="contMyIP"]').val('');
+ $('.myMAC').hide();
+ $('input[name="contMyMAC"]').val('');
} else {
$('.myIP').show();
$('#myIP').html('=_('Subnet')?>: '+subnet[bridge]);
+ $('.myMAC').show();
$('.netCONT').hide();
$('#netCONT').val('');
}
@@ -1932,4 +1953,3 @@ function load_contOverview() {
}
-
diff --git a/emhttp/plugins/dynamix.docker.manager/include/Helpers.php b/emhttp/plugins/dynamix.docker.manager/include/Helpers.php
index 2b5425128..3df738c90 100644
--- a/emhttp/plugins/dynamix.docker.manager/include/Helpers.php
+++ b/emhttp/plugins/dynamix.docker.manager/include/Helpers.php
@@ -32,6 +32,66 @@ function xml_decode($string) {
return strval(html_entity_decode($string, ENT_XML1, 'UTF-8'));
}
+function extraParamsWithQuotedValuesMasked($extraParams) {
+ return preg_replace('/"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\']*\'/', '""', $extraParams);
+}
+
+function replaceUnquotedExtraParams($extraParams, $callback) {
+ $parts = preg_split('/("[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\']*\')/', $extraParams, -1, PREG_SPLIT_DELIM_CAPTURE);
+ if ($parts === false) {
+ return $extraParams;
+ }
+ foreach ($parts as $i => $part) {
+ if ($part === '' || $part[0] === '"' || $part[0] === "'") {
+ continue;
+ }
+ $parts[$i] = $callback($part);
+ }
+ return implode('', $parts);
+}
+
+function extractMacAddressParam($extraParams) {
+ if (!is_string($extraParams)) {
+ return '';
+ }
+ $extraParams = extraParamsWithQuotedValuesMasked($extraParams);
+ if (preg_match('/(?:^|\s)--mac-address=([^\s\'"]+)/', $extraParams, $match)) {
+ return trim($match[1]);
+ }
+ if (preg_match('/(?:^|\s)--mac-address\s+([^\s\'"]+)/', $extraParams, $match)) {
+ return trim($match[1]);
+ }
+ return '';
+}
+
+function removeMacAddressParam($extraParams) {
+ if (!is_string($extraParams) || $extraParams === '') {
+ return '';
+ }
+ $extraParams = replaceUnquotedExtraParams($extraParams, function($part) {
+ $part = preg_replace('/(^|\s)--mac-address=[^\s\'"]+/', '$1', $part);
+ return preg_replace('/(^|\s)--mac-address\s+[^\s\'"]+/', '$1', $part);
+ });
+ return trim($extraParams);
+}
+
+function hasNetworkParam($extraParams) {
+ return is_string($extraParams) && preg_match('/(?:^|\s)--net(?:work)?(?:=|\s+)[^\s\'"]+/', extraParamsWithQuotedValuesMasked($extraParams));
+}
+
+function normalizeMacAddress($mac) {
+ $mac = strtolower(trim($mac ?? ''));
+ if ($mac === '') {
+ return '';
+ }
+ if (preg_match('/^[0-9a-f]{12}$/', $mac)) {
+ $mac = implode(':', str_split($mac, 2));
+ } else {
+ $mac = str_replace('-', ':', $mac);
+ }
+ return preg_match('/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/', $mac) ? $mac : '';
+}
+
function generateTSwebui($url, $serve, $webUI) {
if (!isset($webUI)) {
return '';
@@ -75,6 +135,9 @@ function postToXML($post, $setOwnership=false) {
$xml->Network = xml_encode($post['contNetwork']);
}
$xml->MyIP = xml_encode($post['contMyIP']);
+ $extraNetwork = hasNetworkParam($post['contExtraParams'] ?? '');
+ $myMAC = $extraNetwork ? '' : normalizeMacAddress(trim($post['contMyMAC'] ?? '') ?: extractMacAddressParam($post['contExtraParams'] ?? ''));
+ $xml->MyMAC = xml_encode($myMAC);
$xml->Shell = xml_encode($post['contShell']);
$xml->Privileged = strtolower($post['contPrivileged']??'')=='on' ? 'true' : 'false';
$xml->Support = xml_encode($post['contSupport']);
@@ -85,7 +148,7 @@ function postToXML($post, $setOwnership=false) {
$xml->WebUI = xml_encode(trim($post['contWebUI']));
$xml->TemplateURL = xml_encode($post['contTemplateURL']);
$xml->Icon = xml_encode(trim($post['contIcon']));
- $xml->ExtraParams = xml_encode($post['contExtraParams']);
+ $xml->ExtraParams = xml_encode($myMAC && !$extraNetwork ? removeMacAddressParam($post['contExtraParams']) : $post['contExtraParams']);
$xml->PostArgs = xml_encode($post['contPostArgs']);
$xml->CPUset = xml_encode($post['contCPUset']);
$xml->DateInstalled = xml_encode(time());
@@ -149,6 +212,9 @@ function xmlToVar($xml) {
$out['Registry'] = xml_decode($xml->Registry);
$out['Network'] = xml_decode($xml->Network);
$out['MyIP'] = xml_decode($xml->MyIP ?? '');
+ $extraParams = xml_decode($xml->ExtraParams ?? '');
+ $extraNetwork = hasNetworkParam($extraParams);
+ $out['MyMAC'] = $extraNetwork ? '' : normalizeMacAddress(xml_decode($xml->MyMAC ?? '') ?: extractMacAddressParam($extraParams));
$out['Shell'] = xml_decode($xml->Shell ?? 'sh');
$out['Privileged'] = xml_decode($xml->Privileged);
$out['Support'] = xml_decode($xml->Support);
@@ -159,7 +225,7 @@ function xmlToVar($xml) {
$out['WebUI'] = xml_decode($xml->WebUI);
$out['TemplateURL'] = xml_decode($xml->TemplateURL);
$out['Icon'] = xml_decode($xml->Icon);
- $out['ExtraParams'] = xml_decode($xml->ExtraParams);
+ $out['ExtraParams'] = $extraParams;
$out['PostArgs'] = xml_decode($xml->PostArgs);
$out['CPUset'] = xml_decode($xml->CPUset);
$out['DonateText'] = xml_decode($xml->DonateText);
@@ -325,13 +391,29 @@ function xmlToCommand($xml, $create_paths=false) {
$xml = xmlToVar($xml);
$cmdName = strlen($xml['Name']) ? '--name='.escapeshellarg($xml['Name']) : '';
$cmdPrivileged = strtolower($xml['Privileged'])=='true' ? '--privileged=true' : '';
+ $extraNetwork = hasNetworkParam($xml['ExtraParams']);
+ $cmdMyIP = '';
if (preg_match('/^container:(.*)/', $xml['Network'])) {
- $cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg($xml['Network']);
+ $cmdNetwork = $extraNetwork ? "" : '--net='.escapeshellarg($xml['Network']);
} else {
- $cmdNetwork = preg_match('/\-\-net(work)?=/',$xml['ExtraParams']) ? "" : '--net='.escapeshellarg(strtolower($xml['Network']));
+ $networkName = strtolower($xml['Network']);
+ if ($extraNetwork) {
+ $cmdNetwork = "";
+ } elseif (strlen($xml['MyMAC']) && !in_array($networkName, ['host','none'])) {
+ $xml['ExtraParams'] = removeMacAddressParam($xml['ExtraParams']);
+ $networkEndpoint = ['name='.$networkName];
+ foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) {
+ if ($myIP) $networkEndpoint[] = (strpos($myIP,':') !== false ? 'ip6=' : 'ip=').$myIP;
+ }
+ $networkEndpoint[] = 'mac-address='.$xml['MyMAC'];
+ $cmdNetwork = '--network='.escapeshellarg(implode(',', $networkEndpoint));
+ } else {
+ $cmdNetwork = '--net='.escapeshellarg($networkName);
+ }
+ }
+ if (!strlen($xml['MyMAC']) || preg_match('/^container:(.*)/', $xml['Network']) || $extraNetwork) {
+ foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) if ($myIP) $cmdMyIP .= (strpos($myIP,':') !== false ? '--ip6=' : '--ip=').escapeshellarg($myIP).' ';
}
- $cmdMyIP = '';
- foreach (explode(' ',str_replace(',',' ',$xml['MyIP'])) as $myIP) if ($myIP) $cmdMyIP .= (strpos($myIP,':')?'--ip6=':'--ip=').escapeshellarg($myIP).' ';
$cmdCPUset = strlen($xml['CPUset']) ? '--cpuset-cpus='.escapeshellarg($xml['CPUset']) : '';
$Volumes = [''];
$Ports = [''];
diff --git a/etc/rc.d/rc.docker b/etc/rc.d/rc.docker
index 631f1a946..a42058798 100755
--- a/etc/rc.d/rc.docker
+++ b/etc/rc.d/rc.docker
@@ -245,9 +245,16 @@ netrestore_connect(){
local MY_TT=$3
local MY_MAC=$4
local MY_IP=
- local MY_OPTS=
+ local MY_IPV4=
+ local MY_IPV6=
local IP=
+ local IPAM_JSON=
+ local ENDPOINT_JSON=
+ local CONNECT_JSON=
+ local CODE=
+ local BODY=
local ENDPOINT_ID=
+ local ENDPOINT_MAC=
local OUT=
container_exist "$CONTAINER" || return 0
@@ -263,23 +270,49 @@ netrestore_connect(){
return 1
fi
- ENDPOINT_ID=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.EndpointID}}{{end}}" "$CONTAINER" 2>/dev/null)
- [[ -n $ENDPOINT_ID ]] && return 0
-
for IP in ${MY_TT//;/ }; do
[[ -n $IP ]] || continue
if [[ $IP =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
+ MY_IPV4=$IP
MY_IP="$MY_IP --ip $IP"
elif [[ $IP =~ : ]]; then
+ MY_IPV6=$IP
MY_IP="$MY_IP --ip6 $IP"
else
log "skipping invalid stored IP for $CONTAINER on network $NETWORK: $IP"
fi
done
- [[ -n $MY_MAC ]] && MY_OPTS="--driver-opt=com.docker.network.endpoint.macaddress=$MY_MAC"
+ ENDPOINT_ID=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.EndpointID}}{{end}}" "$CONTAINER" 2>/dev/null)
+ if [[ -n $ENDPOINT_ID ]]; then
+ [[ -n $MY_MAC ]] || return 0
+ ENDPOINT_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$NETWORK\"}}{{.MacAddress}}{{end}}" "$CONTAINER" 2>/dev/null)
+ [[ ${ENDPOINT_MAC,,} == ${MY_MAC,,} ]] && return 0
+ log "reconnecting $CONTAINER to network $NETWORK to restore MAC $MY_MAC"
+ if ! OUT=$(docker network disconnect -f "$NETWORK" "$CONTAINER" 2>&1); then
+ log "failed to disconnect $CONTAINER from network $NETWORK: $OUT"
+ return 1
+ fi
+ fi
+
+ if [[ -n $MY_MAC ]]; then
+ [[ -n $MY_IPV4 ]] && IPAM_JSON="\"IPv4Address\":\"$MY_IPV4\""
+ [[ -n $MY_IPV6 ]] && IPAM_JSON="${IPAM_JSON:+$IPAM_JSON,}\"IPv6Address\":\"$MY_IPV6\""
+ ENDPOINT_JSON="\"MacAddress\":\"$MY_MAC\""
+ [[ -n $IPAM_JSON ]] && ENDPOINT_JSON="\"IPAMConfig\":{$IPAM_JSON},$ENDPOINT_JSON"
+ CONNECT_JSON="{\"Container\":\"$CONTAINER\",\"EndpointConfig\":{$ENDPOINT_JSON}}"
+ 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)
+ CODE=${OUT##*$'\n'}
+ BODY=${OUT%$'\n'$CODE}
+ if [[ $CODE != 2* ]]; then
+ log "failed to connect $CONTAINER to network $NETWORK: $BODY"
+ return 1
+ fi
+ return 0
+ fi
+
log "connecting $CONTAINER to network $NETWORK"
- if ! OUT=$(docker network connect $MY_OPTS $MY_IP $NETWORK $CONTAINER 2>&1); then
+ if ! OUT=$(docker network connect $MY_IP $NETWORK $CONTAINER 2>&1); then
log "failed to connect $CONTAINER to network $NETWORK: $OUT"
return 1
fi
@@ -347,20 +380,25 @@ docker_network_start(){
REBUILD=1
fi
done
- MY_NETWORK= MY_IP= MY_MAC= TEMPLATE_MAC= CUSTOM_PRIMARY=
+ MY_NETWORK= MY_IP= MY_MAC= XML_MAC= TEMPLATE_MAC= CUSTOM_PRIMARY=
while read_dom; do
[[ $ENTITY == Network ]] && MY_NETWORK=$CONTENT
[[ $ENTITY == MyIP ]] && MY_IP=${CONTENT// /,} && MY_IP=$(echo "$MY_IP" | tr -s "," ";")
+ [[ $ENTITY == MyMAC ]] && XML_MAC=${CONTENT// /}
done <$XMLFILE
# only restore valid networks
if [[ -n $MY_NETWORK ]]; then
[[ $MY_NETWORK =~ ^(br|bond|eth|wlan)[0-9]+(\.[0-9]+)?$ ]] && CUSTOM_PRIMARY=1
TEMPLATE_MAC=$(sed -nE 's@.*.*--mac-address(=|[[:space:]]+)([^ <]+).*@\2@p' "$XMLFILE" | head -n1)
- MY_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$MY_NETWORK\"}}{{.MacAddress}}{{end}}" $CONTAINER 2>/dev/null)
- [[ -n $MY_MAC ]] || MY_MAC=$TEMPLATE_MAC
+ if [[ -n $XML_MAC ]]; then
+ MY_MAC=$XML_MAC
+ else
+ MY_MAC=$(docker inspect --format="{{with index .NetworkSettings.Networks \"$MY_NETWORK\"}}{{.MacAddress}}{{end}}" $CONTAINER 2>/dev/null)
+ [[ -n $MY_MAC ]] || MY_MAC=$TEMPLATE_MAC
+ fi
netrestore_add "$MY_NETWORK" "$CONTAINER" "$MY_IP" "$MY_MAC"
PRIMARY_NETWORK[$CONTAINER]=$MY_NETWORK
- [[ -n $REBUILD || (-n $TEMPLATE_MAC && -n $CUSTOM_PRIMARY) ]] && REBUILD_CONTAINERS[$CONTAINER]=1
+ [[ -n $REBUILD || (-z $XML_MAC && -n $TEMPLATE_MAC && -n $CUSTOM_PRIMARY) ]] && REBUILD_CONTAINERS[$CONTAINER]=1
fi
fi
# restore user defined networks