diff --git a/src/main/java/it/inaf/oats/vospace/BaseNodeController.java b/src/main/java/it/inaf/oats/vospace/BaseNodeController.java
index 81e2a8b6c5eb1021e55cdcd9b76176c57b54be2b..d203e2f8491448a8bc0973031e7dbdad42779100 100644
--- a/src/main/java/it/inaf/oats/vospace/BaseNodeController.java
+++ b/src/main/java/it/inaf/oats/vospace/BaseNodeController.java
@@ -19,11 +19,11 @@ public abstract class BaseNodeController {
 
     @Autowired
     private HttpServletRequest servletRequest;
-    
+
     @Value("${vospace-authority}")
     protected String authority;
-        
-    protected String getPath() {       
+
+    protected String getPath() {
         String requestURL = servletRequest.getRequestURL().toString();
         try {
             return NodeUtils.getPathFromRequestURLString(requestURL);
@@ -35,7 +35,7 @@ public abstract class BaseNodeController {
     protected String getParentPath(String path) {
         return NodeUtils.getParentPath(path);
     }
-    
+
     protected void validateAndCheckPayloadURIConsistence(Node node) {
         // Get Node path (and validates it too)
         String decodedURIPathFromNode = URIUtils.returnVosPathFromNodeURI(node.getUri(), this.authority);
@@ -45,16 +45,27 @@ public abstract class BaseNodeController {
         if (!decodedURIPathFromNode.equals(this.getPath())) {
             throw new InvalidURIException(decodedURIPathFromNode, requestPath);
         }
-        
+
     }
-    
-    protected void validateInternalLinkNode(LinkNode linkNode) {
+
+    protected void validateLinkNode(LinkNode linkNode) {
         String target = linkNode.getTarget();
         // I validate it here to add context easily
         if (target == null) {
             throw new InvalidArgumentException("LinkNode in payload has no target element specified");
         }
 
-        URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority);
+        if (URIUtils.isURIInternal(target)) {
+            URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority);
+        } else {
+            // TODO: Let's discuss if we need to combine this validation with
+            // protocol endpoints management (URIService, ProtocolType)
+            // Let's start with http and https only for now
+            if (!(target.toLowerCase().startsWith("http://")
+                    || target.toLowerCase().startsWith("https://"))) {
+                throw new InvalidArgumentException("LinkNode target malformed or unsupported protocol: " + target);
+            }
+
+        }
     }
 }
diff --git a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java
index 4cd2860cf793f73eb7c04a0e4243e3db7f835573..4fa613ff8393b72c1901afa5f9c047ade6291bf4 100644
--- a/src/main/java/it/inaf/oats/vospace/CreateNodeController.java
+++ b/src/main/java/it/inaf/oats/vospace/CreateNodeController.java
@@ -6,7 +6,6 @@
 package it.inaf.oats.vospace;
 
 import it.inaf.ia2.aa.data.User;
-import it.inaf.oats.vospace.exception.InvalidURIException;
 import net.ivoa.xml.vospace.v2.LinkNode;
 import net.ivoa.xml.vospace.v2.Node;
 import org.springframework.http.MediaType;
@@ -45,7 +44,7 @@ public class CreateNodeController extends BaseNodeController {
     private void validateInputNode(Node node) {
 
         if (node instanceof LinkNode) {
-            this.validateInternalLinkNode((LinkNode) node);
+            this.validateLinkNode((LinkNode) node);
         }
 
     }
diff --git a/src/main/java/it/inaf/oats/vospace/FileServiceClient.java b/src/main/java/it/inaf/oats/vospace/FileServiceClient.java
index f23a4b293c6496f8802ae2c017af658fa00f3fbe..89d6912f26df618b4b97dcb1049d0fd0adcc6804 100644
--- a/src/main/java/it/inaf/oats/vospace/FileServiceClient.java
+++ b/src/main/java/it/inaf/oats/vospace/FileServiceClient.java
@@ -7,12 +7,19 @@ package it.inaf.oats.vospace;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.aa.data.User;
+import it.inaf.oats.vospace.datamodel.NodeUtils;
 import it.inaf.oats.vospace.datamodel.Views;
 import it.inaf.oats.vospace.exception.InvalidArgumentException;
+import it.inaf.oats.vospace.exception.NodeNotFoundException;
+import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor;
+import it.inaf.oats.vospace.persistence.NodeDAO;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.ContainerNode;
+import net.ivoa.xml.vospace.v2.Node;
 import net.ivoa.xml.vospace.v2.Transfer;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -21,6 +28,7 @@ import org.springframework.http.HttpMethod;
 import org.springframework.http.MediaType;
 import org.springframework.stereotype.Component;
 import org.springframework.web.client.RestTemplate;
+import static org.springframework.web.servlet.mvc.method.RequestMappingInfo.paths;
 
 @Component
 public class FileServiceClient {
@@ -39,6 +47,12 @@ public class FileServiceClient {
     @Autowired
     private HttpServletRequest request;
 
+    @Autowired
+    private LinkService linkService;
+
+    @Autowired
+    private NodeDAO nodeDAO;
+
     public String startArchiveJob(Transfer transfer, String jobId) {
 
         String target = transfer.getTarget().substring("vos://".length() + authority.length());
@@ -64,9 +78,38 @@ public class FileServiceClient {
             vosPaths.add(target);
         }
 
+        // Generate descriptors
+        // Expand container nodes into direct children list
+        List<String> expandedVosPaths = new ArrayList<>();
+
+        for (String vosPath : vosPaths) {
+            Node node
+                    = nodeDAO.listNode(vosPath)
+                            .orElseThrow(() -> {
+                                throw new NodeNotFoundException(vosPath);
+                            });
+
+            if (node instanceof ContainerNode) {
+                List<Node> nodes = ((ContainerNode) node).getNodes();
+                if (nodes.isEmpty()) {
+                    expandedVosPaths.add(NodeUtils.getVosPath(node));
+                } else {
+                    expandedVosPaths.addAll(nodes
+                            .stream().map(n -> NodeUtils.getVosPath(n))
+                            .collect(Collectors.toList()));
+                }
+            } else {
+                expandedVosPaths.add(vosPath);
+            }
+
+        }
+
+        // follow links to links in vosPaths
+        List<ArchiveEntryDescriptor> entryDescriptors = linkService.followLinksForArchiveService(expandedVosPaths);
+
         ArchiveRequest archiveRequest = new ArchiveRequest();
         archiveRequest.setJobId(jobId);
-        archiveRequest.setPaths(vosPaths);
+        archiveRequest.setEntryDescriptors(entryDescriptors);
         archiveRequest.setType(archiveTypeFromViewUri(transfer.getView().getUri()));
 
         String url = fileServiceUrl + "/archive";
@@ -86,17 +129,17 @@ public class FileServiceClient {
             return res.getHeaders().getLocation().toString();
         }, new Object[]{});
     }
-    
-    public void startFileCopyJob(String sourceVosPath, 
+
+    public void startFileCopyJob(String sourceVosPath,
             String destiantionVosPath, String jobId, User user) {
-        
+
         CopyRequest copyRequest = new CopyRequest();
         copyRequest.setJobId(jobId);
         copyRequest.setSourceRootVosPath(sourceVosPath);
         copyRequest.setDestinationRootVosPath(destiantionVosPath);
-        
+
         String url = fileServiceUrl + "/copy";
-        
+
         String token = user.getAccessToken();
         restTemplate.execute(url, HttpMethod.POST, req -> {
             HttpHeaders headers = req.getHeaders();
@@ -108,10 +151,10 @@ public class FileServiceClient {
             try (OutputStream os = req.getBody()) {
                 MAPPER.writeValue(os, copyRequest);
             }
-        }, res -> {           
-           return null;
+        }, res -> {
+            return null;
         }, new Object[]{});
-        
+
     }
 
     public static class CopyRequest {
@@ -150,7 +193,7 @@ public class FileServiceClient {
 
         private String type;
         private String jobId;
-        private List<String> paths;
+        private List<ArchiveEntryDescriptor> entryDescriptors;
 
         public String getType() {
             return type;
@@ -168,12 +211,12 @@ public class FileServiceClient {
             this.jobId = jobId;
         }
 
-        public List<String> getPaths() {
-            return paths;
+        public List<ArchiveEntryDescriptor> getEntryDescriptors() {
+            return entryDescriptors;
         }
 
-        public void setPaths(List<String> paths) {
-            this.paths = paths;
+        public void setEntryDescriptors(List<ArchiveEntryDescriptor> entryDescriptors) {
+            this.entryDescriptors = entryDescriptors;
         }
     }
 
diff --git a/src/main/java/it/inaf/oats/vospace/LinkService.java b/src/main/java/it/inaf/oats/vospace/LinkService.java
new file mode 100644
index 0000000000000000000000000000000000000000..729c643a34a5d83eefc9b5972fbe6b76b9455760
--- /dev/null
+++ b/src/main/java/it/inaf/oats/vospace/LinkService.java
@@ -0,0 +1,105 @@
+/*
+ * This file is part of vospace-rest
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.oats.vospace;
+
+import it.inaf.oats.vospace.datamodel.NodeUtils;
+import it.inaf.oats.vospace.exception.InternalFaultException;
+import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor;
+import it.inaf.oats.vospace.persistence.NodeDAO;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import net.ivoa.xml.vospace.v2.LinkNode;
+import net.ivoa.xml.vospace.v2.Node;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+@Service
+public class LinkService {
+
+    @Value("${vospace-authority}")
+    private String authority;
+
+    @Value("${link-max-depth}")
+    private int linkMaxDepth;
+    
+    @Autowired
+    private NodeDAO nodeDao;
+    
+    // Returns a new list = the list in argument with paths of links to links are substituted with 
+    // their actual destination vos path. Only links to external resources 
+    // (http:// ... ) should remain
+    public List<ArchiveEntryDescriptor> followLinksForArchiveService(List<String> vosPaths) {
+ 
+        List<LinkNode> linkNodesInPaths = nodeDao.returnLinkNodesInList(vosPaths);
+        
+        // No links no change
+        if(linkNodesInPaths.isEmpty())
+            return vosPaths.stream().map(p -> new ArchiveEntryDescriptor(p))
+                    .collect(Collectors.toList());
+              
+        List<String> linkVosPaths = linkNodesInPaths.stream()
+                .map(ln -> NodeUtils.getVosPath(ln)).collect(Collectors.toList());
+        
+        // Safe copy of argument        
+        List<String> resultVosPaths = new ArrayList<>(vosPaths);
+        
+        resultVosPaths.removeAll(linkVosPaths);
+        
+        // Generate descriptors from non link vospaths
+        List<ArchiveEntryDescriptor> resultDescriptors = 
+                resultVosPaths.stream().map(p -> new ArchiveEntryDescriptor(p))
+                        .collect(Collectors.toList());
+        
+        // Add descriptors from links
+        resultDescriptors.addAll(
+                linkNodesInPaths.stream().map(p -> getLinkNodeArchiveEntryDescriptor(p))
+                .collect(Collectors.toList())       
+        );              
+        
+        return resultDescriptors;
+                                    
+    }
+    
+    private ArchiveEntryDescriptor getLinkNodeArchiveEntryDescriptor(LinkNode node){
+        String vosPath = NodeUtils.getVosPath(node);        
+        String targetNodeVosPath = NodeUtils.getVosPath(this.followLink(node));
+        
+        return new ArchiveEntryDescriptor(vosPath, targetNodeVosPath);
+    }
+    
+    public Node followLink(LinkNode linkNode) {
+        return this.followLinkRecursive(linkNode, 0);
+    }
+
+    private Node followLinkRecursive(LinkNode linkNode, int depth) {
+
+        if (depth >= linkMaxDepth) {
+            throw new InternalFaultException("Max link depth reached at link node: "
+                    + NodeUtils.getVosPath(linkNode));
+        }
+
+        String linkTarget = linkNode.getTarget();
+
+        if (URIUtils.isURIInternal(linkTarget)) {
+            String targetPath = URIUtils.returnVosPathFromNodeURI(linkTarget, authority);
+
+            Optional<Node> targetNodeOpt = nodeDao.listNode(targetPath);
+            Node targetNode = targetNodeOpt.orElseThrow(() -> new InternalFaultException("Broken Link to target: " + targetPath));
+
+            if (targetNode instanceof LinkNode) {
+                return this.followLinkRecursive(linkNode, ++depth);
+            } else {
+                return targetNode;
+            }
+        } else {
+            return linkNode;
+        }
+    }
+
+}
diff --git a/src/main/java/it/inaf/oats/vospace/SetNodeController.java b/src/main/java/it/inaf/oats/vospace/SetNodeController.java
index 7659ab947f53823a356ff7a46328236d190cdcd2..aa712ecb4f8737778f4067b12e0be824e3d93c55 100644
--- a/src/main/java/it/inaf/oats/vospace/SetNodeController.java
+++ b/src/main/java/it/inaf/oats/vospace/SetNodeController.java
@@ -73,7 +73,7 @@ public class SetNodeController extends BaseNodeController {
         if (node instanceof DataNode) {
             checkViews((DataNode) node, (DataNode) toBeModifiedNode);
         } else if(node instanceof LinkNode) {
-            this.validateInternalLinkNode((LinkNode) node);            
+            this.validateLinkNode((LinkNode) node);            
         }
 
         //The service SHOULD throw a HTTP 500 status code including an InternalFault fault 
diff --git a/src/main/java/it/inaf/oats/vospace/URIUtils.java b/src/main/java/it/inaf/oats/vospace/URIUtils.java
index 0250050fd271a772725b8a1097f4a63a367c1f94..36bd239aff98a538cf34d445f402d049577c0465 100644
--- a/src/main/java/it/inaf/oats/vospace/URIUtils.java
+++ b/src/main/java/it/inaf/oats/vospace/URIUtils.java
@@ -18,6 +18,13 @@ public class URIUtils {
     // Slashes are treated separately
     private static final Pattern FORBIDDEN_CHARS = Pattern.compile("[\\x00\\x08\\x0B\\x0C\\x0E-\\x1F" + Pattern.quote("<>?\":\\|'`*") + "]");
     private static final String SCHEME = "vos";
+    
+    public static boolean isURIInternal(String URI) {
+        if(URI == null)
+            throw new IllegalArgumentException("URI can't be null");
+        
+        return URI.toLowerCase().startsWith(SCHEME);
+    }
 
     public static String returnURIFromVosPath(String vosPath, String authority) {
         String result = null;
diff --git a/src/main/java/it/inaf/oats/vospace/UriService.java b/src/main/java/it/inaf/oats/vospace/UriService.java
index 056aa04248b782adcbdd0576ecb876e8d7256b46..aacc7fa2c8bc477b7c50b7cfd3a8fab2797b9081 100644
--- a/src/main/java/it/inaf/oats/vospace/UriService.java
+++ b/src/main/java/it/inaf/oats/vospace/UriService.java
@@ -19,6 +19,7 @@ import it.inaf.oats.vospace.exception.NodeNotFoundException;
 import it.inaf.oats.vospace.exception.PermissionDeniedException;
 import it.inaf.oats.vospace.exception.ProtocolNotSupportedException;
 import it.inaf.oats.vospace.exception.NodeBusyException;
+import it.inaf.oats.vospace.parent.persistence.LinkedServiceDAO;
 import it.inaf.oats.vospace.persistence.LocationDAO;
 import it.inaf.oats.vospace.persistence.NodeDAO;
 import it.inaf.oats.vospace.persistence.model.Location;
@@ -27,6 +28,7 @@ import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
@@ -49,15 +51,18 @@ public class UriService {
     @Value("${file-service-url}")
     private String fileServiceUrl;
 
-    @Value("${link-max-depth}")
-    private int linkMaxDepth;
-
     @Autowired
     private NodeDAO nodeDao;
+    
+    @Autowired
+    private LinkService linkService;
 
     @Autowired
     private LocationDAO locationDAO;
 
+    @Autowired
+    private LinkedServiceDAO linkedServiceDAO;
+
     @Autowired
     private HttpServletRequest servletRequest;
 
@@ -90,14 +95,15 @@ public class UriService {
         JobService.JobDirection jobDirection
                 = JobDirection.getJobDirectionEnumFromTransfer(transfer);
 
-        List<String> validProtocolUris = new ArrayList<>();
+        List<ProtocolType> validProtocolTypes = new ArrayList<>();
         switch (jobDirection) {
             case pullFromVoSpace:
+                validProtocolTypes.add(ProtocolType.HTTPSGET);
             case pullToVoSpace:
-                validProtocolUris.add("ivo://ivoa.net/vospace/core#httpget");
+                validProtocolTypes.add(ProtocolType.HTTPGET);
                 break;
             case pushToVoSpace:
-                validProtocolUris.add("ivo://ivoa.net/vospace/core#httpput");
+                validProtocolTypes.add(ProtocolType.HTTPPUT);
                 break;
 
             default:
@@ -106,15 +112,30 @@ public class UriService {
 
         List<Protocol> validProtocols
                 = transfer.getProtocols().stream()
-                        // discard invalid protocols
-                        .filter(protocol -> validProtocolUris.contains(protocol.getUri()))
+                        // discard invalid protocols by uri String
+                        .filter(protocol
+                                -> validProtocolTypes.stream().map(pt
+                                -> {
+                            return pt.getUri();
+                        })
+                                .collect(Collectors.toList())
+                                .contains(protocol.getUri()))
                         .map(p -> {
                             // set endpoints
-                            Protocol protocol = new Protocol();
-                            protocol.setUri(p.getUri());
-                            protocol.setEndpoint(getEndpoint(job, transfer));
-                            return protocol;
-                        }).collect(Collectors.toList());
+                            String endpoint = getEndpoint(job, transfer);
+                            ProtocolType pt
+                                    = ProtocolType.getProtocolTypeFromURI(p.getUri());
+
+                            if (pt.isEndpointCompliant(endpoint)) {
+                                Protocol protocol = new Protocol();
+                                protocol.setUri(p.getUri());
+                                protocol.setEndpoint(endpoint);
+                                return protocol;
+                            } else {
+                                return null;
+                            }
+                        }).filter(Objects::nonNull)
+                        .collect(Collectors.toList());
 
         if (validProtocols.isEmpty()) {
             Protocol protocol = transfer.getProtocols().get(0);
@@ -137,7 +158,7 @@ public class UriService {
                     if (!NodeUtils.checkIfReadable(node, user.getName(), user.getGroups())) {
                         throw PermissionDeniedException.forPath(relativePath);
                     }
-                    node = this.followLink((LinkNode) node);
+                    node = linkService.followLink((LinkNode) node);
                 }
             }
             return node;
@@ -168,7 +189,7 @@ public class UriService {
         JobService.JobDirection jobType
                 = JobDirection.getJobDirectionEnumFromTransfer(transfer);
         Node node = this.getEndpointNode(relativePath, jobType, user);
-               
+
         switch (jobType) {
             case pushToVoSpace:
             case pullToVoSpace:
@@ -178,7 +199,7 @@ public class UriService {
                 break;
 
             case pullFromVoSpace:
-                // Refresh relative path: it can differ in case of links
+                // Refresh relative path: it can differ in case of links followed 
                 relativePath = NodeUtils.getVosPath(node);
                 if (!NodeUtils.checkIfReadable(node, creator, groups)) {
                     throw PermissionDeniedException.forPath(relativePath);
@@ -197,25 +218,36 @@ public class UriService {
             return fileServiceClient.startArchiveJob(transfer, job.getJobId());
         }
 
-        Location location = locationDAO.getNodeLocation(relativePath).orElse(null);
+        boolean isLinkNode = node instanceof LinkNode;
 
         String endpoint;
 
-        if (location != null && location.getType() == LocationType.PORTAL) {
-            String fileName = nodeDao.getNodeOsName(relativePath);
-            endpoint = "http://" + location.getSource().getHostname() + location.getSource().getBaseUrl();
-            if (!endpoint.endsWith("/")) {
-                endpoint += "/";
+        if (isLinkNode) {
+            endpoint = ((LinkNode) node).getTarget();
+            String linkTarget = ((LinkNode) node).getTarget();
+            if (linkedServiceDAO.isLinkedServiceUrl(linkTarget)) {
+                endpoint += "?token=" + getEndpointToken(linkTarget);
             }
-            endpoint += fileName;
         } else {
-            endpoint = fileServiceUrl + urlEncodePath(relativePath);
-        }
 
-        endpoint += "?jobId=" + job.getJobId();
+            Location location = locationDAO.getNodeLocation(relativePath).orElse(null);
 
-        if (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) {
-            endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath);
+            if (location != null && location.getType() == LocationType.PORTAL) {
+                String fileName = nodeDao.getNodeOsName(relativePath);
+                endpoint = "http://" + location.getSource().getHostname() + location.getSource().getBaseUrl();
+                if (!endpoint.endsWith("/")) {
+                    endpoint += "/";
+                }
+                endpoint += fileName;
+            } else {
+                endpoint = fileServiceUrl + urlEncodePath(relativePath);
+            }
+
+            endpoint += "?jobId=" + job.getJobId();
+
+            if (!"true".equals(NodeProperties.getNodePropertyByURI(node, NodeProperties.PUBLIC_READ_URI))) {
+                endpoint += "&token=" + getEndpointToken(fileServiceUrl + relativePath);
+            }
         }
 
         return endpoint;
@@ -282,26 +314,40 @@ public class UriService {
         return (Transfer) job.getJobInfo().getAny().get(0);
     }
 
-    private Node followLink(LinkNode linkNode) {
-        return this.followLinkRecursive(linkNode, 0);
-    }
+    public enum ProtocolType {
+        // Please keep the URIs in this enum UNIQUE!
+        // added a unit test to check this
+        HTTPGET("ivo://ivoa.net/vospace/core#httpget", "http"),
+        HTTPSGET("ivo://ivoa.net/vospace/core#httpsget", "https"),
+        HTTPPUT("ivo://ivoa.net/vospace/core#httpput", "http"),
+        HTTPSPUT("ivo://ivoa.net/vospace/core#httpsput", "https");
+
+        private final String uri;
+        private final String protocolString;
+
+        private ProtocolType(String uri, String protocolString) {
+            this.uri = uri;
+            this.protocolString = protocolString;
+        }
 
-    private Node followLinkRecursive(LinkNode linkNode, int depth) {
-                
-        if(depth >= linkMaxDepth) {
-            throw new InternalFaultException("Max link depth reached at link node: " 
-                    + NodeUtils.getVosPath(linkNode));
+        public String getUri() {
+            return this.uri;
         }
-        
-        String targetPath = URIUtils.returnVosPathFromNodeURI(linkNode.getTarget(), authority);
-        
-        Optional<Node> targetNodeOpt = nodeDao.listNode(targetPath);
-        Node targetNode = targetNodeOpt.orElseThrow(() -> new InternalFaultException("Broken Link to target: " + targetPath));
-        
-        if(targetNode instanceof LinkNode) {            
-            return this.followLinkRecursive(linkNode, ++depth);
-        } else {            
-            return targetNode;
+
+        public boolean isEndpointCompliant(String endpoint) {
+            return endpoint.toLowerCase()
+                    .startsWith(this.protocolString + "://");
         }
+
+        public static ProtocolType getProtocolTypeFromURI(String uri) {
+            for (ProtocolType pt : ProtocolType.values()) {
+                if (pt.getUri().equals(uri)) {
+                    return pt;
+                }
+            }
+
+            return null;
+        }
+
     }
 }
diff --git a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
index ed25fc38d4124abc84ec3d2b5a2afd1f9ba1cf38..e1eac2941bf912ac23cc41aac8b07888e121fa37 100644
--- a/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
+++ b/src/main/java/it/inaf/oats/vospace/persistence/NodeDAO.java
@@ -18,6 +18,7 @@ import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -174,7 +175,7 @@ public class NodeDAO {
                 ps.setArray(++i, fromPropertyToArray(ps, NodeProperties.getNodePropertyByURI(newNode, NodeProperties.GROUP_WRITE_URI)));
                 ps.setBoolean(++i, Boolean.valueOf(NodeProperties.getNodePropertyByURI(newNode, NodeProperties.PUBLIC_READ_URI)));
                 if (isLinkNode) {
-                    ps.setString(++i, ((LinkNode) newNode).getTarget() );
+                    ps.setString(++i, ((LinkNode) newNode).getTarget());
                 }
                 ps.setString(++i, vosPath);
                 return ps;
@@ -552,6 +553,36 @@ public class NodeDAO {
         }
     }
 
+    public List<LinkNode> returnLinkNodesInList(List<String> vosPaths) {
+
+        if (vosPaths.isEmpty()) {
+            throw new IllegalArgumentException("Received empty list of paths");
+        }
+
+        String sql = "SELECT n.node_id, get_vos_path(n.node_id) as vos_path, n.name,\n"
+                + "n.type, n.async_trans, n.sticky, n.job_id IS NOT NULL AS busy_state, n.creator_id, n.group_read, n.group_write,\n"
+                + "n.is_public, n.content_length, n.created_on, n.last_modified, n.accept_views, n.provide_views, n.quota, n.content_md5, n.target\n"
+                + "FROM node n\n"
+                + "WHERE (" + String.join(" OR ", Collections.nCopies(vosPaths.size(), "n.node_id  = id_from_vos_path(?)")) + ")\n"
+                + "AND n.type = 'link'\n";
+
+        return jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            int i = 0;
+            for (String vosPath : vosPaths) {
+                ps.setString(++i, vosPath);
+            }
+            return ps;
+        }, rs -> {
+            List<LinkNode> linkNodes = new ArrayList<>();
+            while (rs.next()) {
+                linkNodes.add((LinkNode) this.getNodeFromResultSet(rs));
+            }
+            return linkNodes;
+        });
+
+    }
+
     private String getGroupsString(ResultSet rs, String column) throws SQLException {
         Array array = rs.getArray(column);
         if (array == null) {
diff --git a/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java b/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java
index f9dcff79ee7bb9fbbc6ae1928e4ea1ad95796a11..09a78a2e700adce65981c827e67d3ef815db5f55 100644
--- a/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java
+++ b/src/test/java/it/inaf/oats/vospace/CreateNodeControllerTest.java
@@ -242,6 +242,25 @@ public class CreateNodeControllerTest {
         verifyLinkArguments(null);
 
     }
+    
+    @Test
+    public void testCreateLinkNodeExternalHttp() throws Exception {
+        String requestBody = getResourceFileContent("create-link-node-external-http.xml");
+
+        when(nodeDao.listNode(eq("/")))
+                .thenReturn(Optional.of(getContainerParentNode("/")));
+
+        mockMvc.perform(put("/nodes/myExternalHttpLink")
+                .header("Authorization", "Bearer user2_token")
+                .content(requestBody)
+                .contentType(MediaType.APPLICATION_XML)
+                .accept(MediaType.APPLICATION_XML))
+                .andDo(print())
+                .andExpect(status().isOk());
+
+        verifyLinkArguments("http://www.external.com/files/file.txt");
+
+    }
 
     @Test
     public void testNodeAlreadyExisting() throws Exception {
diff --git a/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java b/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java
index b57b7d19a533e08049b6e2581a02f3a33d3480af..9ec78b9fb679cb4250975eb692873f656978d8e2 100644
--- a/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java
+++ b/src/test/java/it/inaf/oats/vospace/FileServiceClientTest.java
@@ -10,11 +10,17 @@ import it.inaf.ia2.aa.data.User;
 import it.inaf.oats.vospace.FileServiceClient.ArchiveRequest;
 import it.inaf.oats.vospace.datamodel.Views;
 import it.inaf.oats.vospace.exception.InvalidArgumentException;
+import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor;
+import it.inaf.oats.vospace.persistence.NodeDAO;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.URI;
+import java.util.List;
+import java.util.Optional;
 import javax.servlet.http.HttpServletRequest;
+import net.ivoa.xml.vospace.v2.ContainerNode;
+import net.ivoa.xml.vospace.v2.DataNode;
 import net.ivoa.xml.vospace.v2.Param;
 import net.ivoa.xml.vospace.v2.Transfer;
 import net.ivoa.xml.vospace.v2.View;
@@ -28,6 +34,7 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -54,6 +61,12 @@ public class FileServiceClientTest {
 
     @Mock
     private HttpServletRequest request;
+    
+    @Mock
+    private LinkService linkService;
+    
+    @Mock
+    private NodeDAO nodeDAO;
 
     @InjectMocks
     private FileServiceClient fileServiceClient;
@@ -63,7 +76,7 @@ public class FileServiceClientTest {
         ReflectionTestUtils.setField(fileServiceClient, "authority", "example.com!vospace");
         ReflectionTestUtils.setField(fileServiceClient, "fileServiceUrl", "http://file-service");
     }
-
+        
     @Test
     public void testTarArchiveJob() {
         testStartArchiveJob(Views.TAR_VIEW_URI);
@@ -92,11 +105,19 @@ public class FileServiceClientTest {
         View view = new View();
         view.setUri(Views.ZIP_VIEW_URI);
         transfer.setView(view);
+        
+        ContainerNode node = Mockito.mock(ContainerNode.class);
+        when(node.getNodes()).thenReturn(List.of());
+        when(node.getUri()).thenReturn("vos://example.com!vospace/mydir");
+        when(nodeDAO.listNode(eq("/mydir"))).thenReturn(Optional.of(node));
+        
+        when(linkService.followLinksForArchiveService(any()))
+                .thenReturn(List.of(new ArchiveEntryDescriptor("/mydir")));
 
         ArchiveRequest archiveRequest = testStartArchiveJob(transfer);
 
-        assertEquals(1, archiveRequest.getPaths().size());
-        assertEquals("/mydir", archiveRequest.getPaths().get(0));
+        assertEquals(1, archiveRequest.getEntryDescriptors().size());
+        assertEquals("/mydir", archiveRequest.getEntryDescriptors().get(0).getTargetNodeVosPath());
     }
 
     @Test
@@ -153,12 +174,27 @@ public class FileServiceClientTest {
         param2.setUri(viewUri + "/include");
         param2.setValue("file2");
         view.getParam().add(param2);
+        
+        DataNode node1 = Mockito.mock(DataNode.class);
+        when(node1.getUri()).thenReturn("vos://example.com!vospace/parent_dir/file1");
+        when(nodeDAO.listNode(eq("/parent_dir/file1"))).thenReturn(Optional.of(node1));
+        
+        DataNode node2 = Mockito.mock(DataNode.class);
+        when(node2.getUri()).thenReturn("vos://example.com!vospace/parent_dir/file2");
+        when(nodeDAO.listNode(eq("/parent_dir/file2"))).thenReturn(Optional.of(node2));
+        
+        when(linkService.followLinksForArchiveService(any())).thenReturn(
+                List.of(new ArchiveEntryDescriptor("/parent_dir/file1"),
+                        new ArchiveEntryDescriptor("/parent_dir/file2")                
+                ));
 
         ArchiveRequest archiveRequest = testStartArchiveJob(transfer);
 
-        assertEquals(2, archiveRequest.getPaths().size());
-        assertEquals("/parent_dir/file1", archiveRequest.getPaths().get(0));
-        assertEquals("/parent_dir/file2", archiveRequest.getPaths().get(1));
+        assertEquals(2, archiveRequest.getEntryDescriptors().size());
+        assertEquals("/parent_dir/file1", 
+                archiveRequest.getEntryDescriptors().get(0).getTargetNodeVosPath());
+        assertEquals("/parent_dir/file2", 
+                archiveRequest.getEntryDescriptors().get(1).getTargetNodeVosPath());
     }
 
     private ArchiveRequest testStartArchiveJob(Transfer transfer) {
diff --git a/src/test/java/it/inaf/oats/vospace/LinkServiceTest.java b/src/test/java/it/inaf/oats/vospace/LinkServiceTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..091e4d0ccd7348c8c494a3b6ec0e0646134025f6
--- /dev/null
+++ b/src/test/java/it/inaf/oats/vospace/LinkServiceTest.java
@@ -0,0 +1,61 @@
+/*
+ * This file is part of vospace-rest
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.oats.vospace;
+
+import it.inaf.ia2.aa.data.User;
+import it.inaf.oats.vospace.exception.NodeBusyException;
+import it.inaf.oats.vospace.exception.NodeNotFoundException;
+import it.inaf.oats.vospace.exception.PermissionDeniedException;
+import it.inaf.oats.vospace.parent.exchange.ArchiveEntryDescriptor;
+import it.inaf.oats.vospace.persistence.DataSourceConfigSingleton;
+import it.inaf.oats.vospace.persistence.NodeDAO;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import net.ivoa.xml.vospace.v2.Transfer;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.ContextConfiguration;
+
+@SpringBootTest
+//@AutoConfigureMockMvc
+@TestPropertySource(locations = "classpath:test.properties", properties = {"vospace-authority=example.com!vospace", "file-service-url=http://file-service"})
+@ContextConfiguration(classes = {DataSourceConfigSingleton.class})
+public class LinkServiceTest {
+
+    @Autowired
+    private LinkService linkService;
+    
+    @Test
+    public void testFollowLinksForArchiveService() {
+        List<String> vosPaths = List.of("/test3/m1/link2", "/test3/m1/m2");
+        
+        List<ArchiveEntryDescriptor> aed = linkService.followLinksForArchiveService(vosPaths);
+        
+        assertEquals(2, aed.size());
+        List<String> targetVosPaths = aed.stream().map(a -> a.getTargetNodeVosPath())
+                .collect(Collectors.toList());
+        
+        System.out.println(targetVosPaths);        
+        assertTrue(targetVosPaths.containsAll(List.of("/test3/m1/m2", "/test1/f1/f2_renamed/f3")));
+                
+    }
+
+    
+}
diff --git a/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java b/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java
index d630f85de6e0161d6216227d8613799dc3b80beb..6897ae542576e46865db1b494bbb116433f54849 100644
--- a/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java
+++ b/src/test/java/it/inaf/oats/vospace/URIUtilsTest.java
@@ -8,7 +8,9 @@ package it.inaf.oats.vospace;
 import it.inaf.oats.vospace.exception.InvalidURIException;
 import java.net.URISyntaxException;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import org.junit.jupiter.api.Test;
 
 public class URIUtilsTest {
@@ -84,6 +86,12 @@ public class URIUtilsTest {
         String test3 = URIUtils.returnURIFromVosPath("/test1/te# !?st2", authority);
         assertEquals("vos://"+authority+"/test1/te%23%20!%3Fst2", test3);       
     }
+    
+    @Test
+    public void testIsURIInternal() {
+        assertTrue(URIUtils.isURIInternal("vos://"+authority+"/test1/test2"));        
+        assertFalse(URIUtils.isURIInternal("http://host.ia2.inaf.it/test1/test2"));
+    } 
 
     
 }
diff --git a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
index b14dc36197da4be78525ce9f350dbc2fc8802d9a..3b98d4fa219678f64c6cff19d9e9cf8527396c5b 100644
--- a/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
+++ b/src/test/java/it/inaf/oats/vospace/UriServiceTest.java
@@ -7,6 +7,7 @@ package it.inaf.oats.vospace;
 
 import it.inaf.ia2.aa.ServletRapClient;
 import it.inaf.ia2.aa.data.User;
+import it.inaf.oats.vospace.UriService.ProtocolType;
 import it.inaf.oats.vospace.datamodel.NodeProperties;
 import it.inaf.oats.vospace.datamodel.NodeUtils;
 import it.inaf.oats.vospace.datamodel.Views;
@@ -15,11 +16,14 @@ import it.inaf.oats.vospace.exception.InvalidArgumentException;
 import it.inaf.oats.vospace.exception.NodeBusyException;
 import it.inaf.oats.vospace.exception.PermissionDeniedException;
 import it.inaf.oats.vospace.exception.ProtocolNotSupportedException;
+import it.inaf.oats.vospace.parent.persistence.LinkedServiceDAO;
 import it.inaf.oats.vospace.persistence.LocationDAO;
 import it.inaf.oats.vospace.persistence.NodeDAO;
 import it.inaf.oats.vospace.persistence.model.Location;
 import it.inaf.oats.vospace.persistence.model.LocationType;
+import java.util.List;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import javax.servlet.http.HttpServletRequest;
 import net.ivoa.xml.uws.v1.JobSummary;
 import net.ivoa.xml.vospace.v2.ContainerNode;
@@ -69,6 +73,9 @@ public class UriServiceTest {
 
     @MockBean
     private CreateNodeService createNodeService;
+    
+    @MockBean
+    private LinkedServiceDAO linkedServiceDAO;
 
     @MockBean
     private FileServiceClient fileServiceClient;
@@ -94,12 +101,15 @@ public class UriServiceTest {
             return request;
         }
     }
-
+    
+    public final static String PORTAL_URL = "http://portalurl.ia2.inaf.it/portal/files";
+    
     @BeforeEach
     public void init() {
         Location location = new Location();
         location.setType(LocationType.ASYNC);
         when(locationDAO.getNodeLocation(any())).thenReturn(Optional.of(location));
+        when(linkedServiceDAO.isLinkedServiceUrl(eq(PORTAL_URL))).thenReturn(Boolean.TRUE);       
     }
 
     @Test
@@ -381,14 +391,7 @@ public class UriServiceTest {
         when(user.getName()).thenReturn("user1");
         
         when(servletRequest.getUserPrincipal()).thenReturn(user);
-
-        
-        when(rapClient.exchangeToken(argThat(req -> {
-            assertEquals("<token>", req.getSubjectToken());
-            assertEquals("http://file-service/mydummydata1", req.getResource());
-            return true;
-        }), any())).thenReturn("<new-token>");
-        
+       
         JobSummary job = getPullFromVoSpaceJob(targetOfPull);
         Transfer tr = uriService.getTransfer(job);
         
@@ -589,9 +592,13 @@ public class UriServiceTest {
         mockPublicNode("parent_dir");
         mockPublicNode("parent_dir/file1");
         mockPublicNode("parent_dir/file2");
+        
+        when(fileServiceClient.startArchiveJob(any(), any()))
+                .thenReturn("http://file-service/mockarchive");
 
         uriService.getNegotiatedTransfer(job, transfer);
-
+        
+        
         verify(fileServiceClient, times(1)).startArchiveJob(transfer, "archive-job-id");
     }
 
@@ -667,4 +674,14 @@ public class UriServiceTest {
 
         return job;
     }
+    
+    @Test
+    public void testProtocolType() {
+        List<ProtocolType> pts = List.of(ProtocolType.values());
+        
+        int distinctUris = 
+                pts.stream().map(pt -> pt.getUri()).distinct().collect(Collectors.toList()).size();
+        
+        assertEquals(pts.size(), distinctUris);
+    }
 }
diff --git a/src/test/resources/create-link-node-external-http.xml b/src/test/resources/create-link-node-external-http.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8046202a52021bb4eaa4b233615dfc25138e36e9
--- /dev/null
+++ b/src/test/resources/create-link-node-external-http.xml
@@ -0,0 +1,11 @@
+<vos:node xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
+          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+          xmlns:vos="http://www.ivoa.net/xml/VOSpace/v2.0" xsi:type="vos:LinkNode" uri="vos://example.com!vospace/myExternalHttpLink">
+    <vos:properties>
+        <vos:property uri="ivo://ivoa.net/vospace/core#description">test value</vos:property>        
+    </vos:properties>
+    <vos:target>http://www.external.com/files/file.txt</vos:target>
+    <vos:accepts/>
+    <vos:provides/>
+    <vos:capabilities/>
+</vos:node>
\ No newline at end of file
diff --git a/src/test/resources/test-data.sql b/src/test/resources/test-data.sql
index 3ae4a612a559bf2d4cdfa798e32f386c98f6a0a1..b9e8f6d18bb59a785e9e6dd52c923bfda64f70e2 100644
--- a/src/test/resources/test-data.sql
+++ b/src/test/resources/test-data.sql
@@ -1,3 +1,5 @@
+INSERT INTO linked_service(service_base_url) VALUES ('http://archives.ia2.inaf.it/files/aao');
+
 INSERT INTO storage (storage_type, base_path, base_url, hostname) VALUES ('cold', '/ia2_tape/users', NULL, 'tape-server');
 INSERT INTO storage (storage_type, base_path, base_url, hostname) VALUES ('hot', '/mnt/hot_storage/users', NULL, 'server');
 INSERT INTO storage (storage_type, base_path, base_url, hostname) VALUES ('local', '/home', NULL, 'localhost');
@@ -41,6 +43,7 @@ INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, gro
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_write, group_read, is_public, location_id) VALUES ('19', '', 'destination2', 'container', 'user3', '{"group1"}', '{"group1"}', false, 3);      -- /mycontainer/destination2
 INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_write, group_read, is_public, location_id) VALUES ('19.21', '20', 'control', 'container', 'user3', '{"group1"}', '{"group1"}', false, 3);      -- /mycontainer/destination2/control
 
+INSERT INTO node (parent_path, parent_relative_path, name, type, creator_id, group_write, group_read, is_public, location_id, target) VALUES ('9.10', '', 'link2', 'link', 'user3', '{"group1"}', '{"group1"}', false, 3, 'vos://example.com!vospace/test1/f1/f2_renamed/f3');      -- /test3/m1/link2
 
 DELETE FROM job;