Repository: yt-dlp/yt-dlp
Pull request #8065 (2023-09-09 00:03:23)

[networking] Fix various bugs in socks proxy implementation
Fixes https://github.com/yt-dlp/yt-dlp/issues/7959
Fixes https://github.com/ytdl-org/youtube-dl/issues/15368

- Fixed support for IPv6 socks proxies
- Fixed support for IPv6 over socks5
- Fixed `--source-address` is not obeyed for either socks4 and socks5
- Fixed Using an IP instead of a domain name for a destination does not work with socks4a

<details open><summary>Template</summary> <!-- OPEN is intentional -->

<!--

# PLEASE FOLLOW THE GUIDE BELOW

- You will be asked some questions, please read them **carefully** and answer honestly
- Put an `x` into all the boxes `[ ]` relevant to your *pull request* (like [x])
- Use *Preview* tab to see how your *pull request* will actually look like

-->

### Before submitting a *pull request* make sure you have:
- [X] At least skimmed through [contributing guidelines](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions) including [yt-dlp coding conventions](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#yt-dlp-coding-conventions)
- [X] [Searched](https://github.com/yt-dlp/yt-dlp/search?q=is%3Apr&type=Issues) the bugtracker for similar pull requests
- [X] Checked the code with [flake8](https://pypi.python.org/pypi/flake8) and [ran relevant tests](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#developer-instructions)

### In order to be accepted and merged into yt-dlp each piece of code must be in public domain or released under [Unlicense](http://unlicense.org/). Check all of the following options that apply:
- [X] I am the original author of this code and I am willing to release it under [Unlicense](http://unlicense.org/)
- [ ] I am not the original author of this code but it is in public domain or released under [Unlicense](http://unlicense.org/) (provide reliable evidence)

### What is the purpose of your *pull request*?
- [ ] Fix or improvement to an extractor (Make sure to add/update tests)
- [ ] New extractor ([Piracy websites will not be accepted](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#is-the-website-primarily-used-for-piracy))
- [X] Core bug fix/improvement
- [ ] New feature (It is strongly [recommended to open an issue first](https://github.com/yt-dlp/yt-dlp/blob/master/CONTRIBUTING.md#adding-new-feature-or-making-overarching-changes))


<!-- Do NOT edit/remove anything below this! -->
</details><details><summary>Copilot Summary</summary>  

<!--
copilot:all
-->
### <samp>🤖 Generated by Copilot at 6801a5f</samp>

### Summary
🐛🌐🧪

<!--
1.  🐛 for fixing bugs
2.  🌐 for adding IPv6 and source_address support
3.  🧪 for improving testing and assertions
-->
This pull request enhances the socks proxy support in `yt-dlp` by adding IPv6 and source_address capabilities, fixing bugs, and improving the testing and implementation of the proxy handlers. It modifies the files `test/test_socks.py`, `yt_dlp/networking/_helper.py`, `yt_dlp/networking/_urllib.py`, and `yt_dlp/socks.py`.

> _We are the proxy handlers, we fix the bugs and flaws_
> _We create connections, we set the source and protocol_
> _We support IPv6, we handle the address families_
> _We are the proxy handlers, we rock the socks off the world_

### Walkthrough
*  Add support for source_address and IPv6 destination addresses in socks proxies ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-2d1e0950dbedb0274cbe77d8492f341f69614fc087ebffa03a1b05daef28f453R210-R253), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-c7b2bfc3ad326c23db07694e9137f86e58e526681dab17e7680a968a7190e232R26), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-c7b2bfc3ad326c23db07694e9137f86e58e526681dab17e7680a968a7190e232L57-R61), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-c7b2bfc3ad326c23db07694e9137f86e58e526681dab17e7680a968a7190e232L223-R208), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL137-R154), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL156-R161), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL213-R218), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL223-R231))
  * Define a new create_connection function in `yt_dlp/networking/_helper.py` that allows specifying the source_address for the socket connection ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-2d1e0950dbedb0274cbe77d8492f341f69614fc087ebffa03a1b05daef28f453R5), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-2d1e0950dbedb0274cbe77d8492f341f69614fc087ebffa03a1b05daef28f453R210-R253))
  * Use the create_connection function in `yt_dlp/networking/_urllib.py` to simplify the _create_http_connection function and to add the _create_connection attribute to the SocksConnection class ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-c7b2bfc3ad326c23db07694e9137f86e58e526681dab17e7680a968a7190e232R26), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-c7b2bfc3ad326c23db07694e9137f86e58e526681dab17e7680a968a7190e232L57-R61), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-c7b2bfc3ad326c23db07694e9137f86e58e526681dab17e7680a968a7190e232L223-R208))
  * Modify the _resolve_address, _setup_socks4, and _setup_socks5 methods in `yt_dlp/socks.py` to handle both IPv4 and IPv6 addresses and to pass the address family information to the proxy ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL137-R154), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL156-R161), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL213-R218), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL223-R231))
* Fix a bug in the socks4a implementation and remove the xfail marks from the corresponding tests ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL156-R161), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL284-R284), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL293-R290))
  * Add a condition to check if the ipaddr is equal to the SOCKS4_DEFAULT_DSTIP in the _setup_socks4 method in `yt_dlp/socks.py`, and only append the domain name to the packet in that case ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-7c9f5be19a1a105113d0887f06e00609701bccf08822868960a3b8c19ed5211cL156-R161))
  * Remove the xfail mark from the test_socks4a_ipv4_target test case in `test/test_socks.py`, and modify the assertion to accept both the ipv4_address and the domain_address as valid ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL284-R284), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL293-R290))
* Remove the xfail marks from the tests that were failing due to the lack of support for source_address and IPv6 in the socks proxies, and modify some assertions to make them more robust ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL305-R301), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL330-R323), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL386-R376), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL407-R397), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL416-R405), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL433-R416))
  * Remove the xfail mark from the test_ipv4_client_source_address test case in `test/test_socks.py`, which was failing for both socks4 and socks5 proxies ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL305-R301), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL433-R416))
  * Remove the xfail mark from the test_ipv6_socks4_proxy test case in `test/test_socks.py`, which was failing for the socks4 proxy ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL330-R323))
  * Remove the xfail mark from the test_socks5_ipv6_destination test case in `test/test_socks.py`, which was failing for the socks5 proxy, and remove the port check from the assertion ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL407-R397), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL416-R405))
  * Modify the assertion in the test_socks5_domain_target test case in `test/test_socks.py`, to accept both the ipv4_address and the ipv6_address as valid ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL386-R376))
* Move the sleep argument from the class level to the handle function level in the Socks4ProxyHandler and Socks5ProxyHandler classes in `test/test_socks.py`, and modify the timeout value in the test_timeout test case ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL83-L85), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcR141-R144), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL156-L158), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL194-R194), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL345-R335))
  * Move the sleep argument from the __init__ method to the handle method in the Socks4ProxyHandler and Socks5ProxyHandler classes, which are used to simulate a proxy server for the tests ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL83-L85), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcR141-R144), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL156-L158), [link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL194-R194))
  * Modify the timeout value in the test_timeout test case in `test/test_socks.py`, to make it more likely to trigger the timeout exception ([link](https://github.com/yt-dlp/yt-dlp/pull/8065/files?diff=unified&w=0#diff-a152f948ac0e3f62133c73ce4b73af89ec4c6639c666729d5a95b5257640f8dcL345-R335))



</details>



Patch:
diff --git a/yt_dlp/extractor/indavideo.py b/yt_dlp/extractor/indavideo.py
index 4fa97d8bb..564bf8a02 100644
--- a/yt_dlp/extractor/indavideo.py
+++ b/yt_dlp/extractor/indavideo.py
@@ -1,9 +1,9 @@
 from .common import InfoExtractor
-from ..compat import compat_str
 from ..utils import (
     int_or_none,
     parse_age_limit,
     parse_iso8601,
+    time_seconds,
     update_url_query,
 )
 
@@ -11,15 +11,14 @@
 class IndavideoEmbedIE(InfoExtractor):
     _VALID_URL = r'https?://(?:(?:embed\.)?indavideo\.hu/player/video/|assets\.indavideo\.hu/swf/player\.swf\?.*\b(?:v(?:ID|id))=)(?P<id>[\da-f]+)'
     # Some example URLs covered by generic extractor:
-    #   http://indavideo.hu/video/Vicces_cica_1
-    #   http://index.indavideo.hu/video/2015_0728_beregszasz
-    #   http://auto.indavideo.hu/video/Sajat_utanfutoban_a_kis_tacsko
-    #   http://erotika.indavideo.hu/video/Amator_tini_punci
-    #   http://film.indavideo.hu/video/f_hrom_nagymamm_volt
-    #   http://palyazat.indavideo.hu/video/Embertelen_dal_Dodgem_egyuttes
-    _EMBED_REGEX = [r'<iframe[^>]+\bsrc=["\'](?P<url>(?:https?:)?//embed\.indavideo\.hu/player/video/[\da-f]+)']
+    #   https://indavideo.hu/video/Vicces_cica_1
+    #   https://index.indavideo.hu/video/Hod_Nemetorszagban
+    #   https://auto.indavideo.hu/video/Sajat_utanfutoban_a_kis_tacsko
+    #   https://film.indavideo.hu/video/f_farkaslesen
+    #   https://palyazat.indavideo.hu/video/Embertelen_dal_Dodgem_egyuttes
+    _EMBED_REGEX = [r'<iframe[^>]+\bsrc=["\'](?P<url>(?:https?:)//embed\.indavideo\.hu/player/video/[\da-f]+)']
     _TESTS = [{
-        'url': 'http://indavideo.hu/player/video/1bdc3c6d80/',
+        'url': 'https://indavideo.hu/player/video/1bdc3c6d80/',
         'md5': 'c8a507a1c7410685f83a06eaeeaafeab',
         'info_dict': {
             'id': '1837039',
@@ -36,21 +35,33 @@ class IndavideoEmbedIE(InfoExtractor):
             'tags': ['tánc', 'cica', 'cuki', 'cukiajanlo', 'newsroom'],
         },
     }, {
-        'url': 'http://embed.indavideo.hu/player/video/1bdc3c6d80?autostart=1&hide=1',
-        'only_matching': True,
-    }, {
-        'url': 'http://assets.indavideo.hu/swf/player.swf?v=fe25e500&vID=1bdc3c6d80&autostart=1&hide=1&i=1',
+        'url': 'https://embed.indavideo.hu/player/video/1bdc3c6d80?autostart=1&hide=1',
         'only_matching': True,
     }]
+    _WEBPAGE_TESTS = [{
+        'url': 'https://indavideo.hu/video/Vicces_cica_1',
+        'info_dict': {
+            'id': '1335611',
+            'ext': 'mp4',
+            'title': 'Vicces cica',
+            'description': 'Játszik a tablettel. :D',
+            'thumbnail': r're:^https?://.*\.jpg$',
+            'uploader': 'Jet_Pack',
+            'uploader_id': '491217',
+            'timestamp': 1390821212,
+            'upload_date': '20140127',
+            'duration': 7,
+            'age_limit': 0,
+            'tags': ['cica', 'Jet_Pack'],
+        },
+    }]
 
     def _real_extract(self, url):
         video_id = self._match_id(url)
 
         video = self._download_json(
-            'https://amfphp.indavideo.hu/SYm0json.php/player.playerHandler.getVideoData/%s' % video_id,
-            video_id)['data']
-
-        title = video['title']
+            f'https://amfphp.indavideo.hu/SYm0json.php/player.playerHandler.getVideoData/{video_id}/',
+            video_id, query={'_': time_seconds()})['data']
 
         video_urls = []
 
@@ -60,33 +71,21 @@ def _real_extract(self, url):
         elif isinstance(video_files, dict):
             video_urls.extend(video_files.values())
 
-        video_file = video.get('video_file')
-        if video:
-            video_urls.append(video_file)
         video_urls = list(set(video_urls))
 
-        video_prefix = video_urls[0].rsplit('/', 1)[0]
-
-        for flv_file in video.get('flv_files', []):
-            flv_url = '%s/%s' % (video_prefix, flv_file)
-            if flv_url not in video_urls:
-                video_urls.append(flv_url)
-
-        filesh = video.get('filesh')
+        filesh = video.get('filesh') or {}
 
         formats = []
         for video_url in video_urls:
             height = int_or_none(self._search_regex(
                 r'\.(\d{3,4})\.mp4(?:\?|$)', video_url, 'height', default=None))
-            if filesh:
-                if not height:
-                    continue
-                token = filesh.get(compat_str(height))
-                if token is None:
-                    continue
-                video_url = update_url_query(video_url, {'token': token})
+            if not height and len(filesh) == 1:
+                height = int_or_none(list(filesh.keys())[0])
+            token = filesh.get(str(height))
+            if token is None:
+                continue
             formats.append({
-                'url': video_url,
+                'url': update_url_query(video_url, {'token': token}),
                 'height': height,
             })
 
@@ -103,7 +102,7 @@ def _real_extract(self, url):
 
         return {
             'id': video.get('id') or video_id,
-            'title': title,
+            'title': video.get('title'),
             'description': video.get('description'),
             'thumbnails': thumbnails,
             'uploader': video.get('user_name'),
diff --git a/yt_dlp/extractor/lecturio.py b/yt_dlp/extractor/lecturio.py
index bb059d3a2..795012541 100644
--- a/yt_dlp/extractor/lecturio.py
+++ b/yt_dlp/extractor/lecturio.py
@@ -57,8 +57,8 @@ class LecturioIE(LecturioBaseIE):
     _VALID_URL = r'''(?x)
                     https://
                         (?:
-                            app\.lecturio\.com/([^/]+/(?P<nt>[^/?#&]+)\.lecture|(?:\#/)?lecture/c/\d+/(?P<id>\d+))|
-                            (?:www\.)?lecturio\.de/[^/]+/(?P<nt_de>[^/?#&]+)\.vortrag
+                            app\.lecturio\.com/([^/?#]+/(?P<nt>[^/?#&]+)\.lecture|(?:\#/)?lecture/c/\d+/(?P<id>\d+))|
+                            (?:www\.)?lecturio\.de/(?:[^/?#]+/)+(?P<nt_de>[^/?#&]+)\.vortrag
                         )
                     '''
     _TESTS = [{
@@ -73,6 +73,9 @@ class LecturioIE(LecturioBaseIE):
     }, {
         'url': 'https://www.lecturio.de/jura/oeffentliches-recht-staatsexamen.vortrag',
         'only_matching': True,
+    }, {
+        'url': 'https://www.lecturio.de/jura/oeffentliches-recht-at-1-staatsexamen/oeffentliches-recht-staatsexamen.vortrag',
+        'only_matching': True,
     }, {
         'url': 'https://app.lecturio.com/#/lecture/c/6434/39634',
         'only_matching': True,
diff --git a/yt_dlp/extractor/massengeschmacktv.py b/yt_dlp/extractor/massengeschmacktv.py
index 7dacb43e0..1490e9b21 100644
--- a/yt_dlp/extractor/massengeschmacktv.py
+++ b/yt_dlp/extractor/massengeschmacktv.py
@@ -17,11 +17,12 @@ class MassengeschmackTVIE(InfoExtractor):
 
     _TEST = {
         'url': 'https://massengeschmack.tv/play/fktv202',
-        'md5': 'a9e054db9c2b5a08f0a0527cc201e8d3',
+        'md5': '9996f314994a49fefe5f39aa1b07ae21',
         'info_dict': {
             'id': 'fktv202',
             'ext': 'mp4',
-            'title': 'Fernsehkritik-TV - Folge 202',
+            'title': 'Fernsehkritik-TV #202',
+            'thumbnail': 'https://cache.massengeschmack.tv/img/mag/fktv202.jpg'
         },
     }
 
@@ -29,9 +30,6 @@ def _real_extract(self, url):
         episode = self._match_id(url)
 
         webpage = self._download_webpage(url, episode)
-        title = clean_html(self._html_search_regex(
-            '<h3>([^<]+)</h3>', webpage, 'title'))
-        thumbnail = self._search_regex(r'POSTER\s*=\s*"([^"]+)', webpage, 'thumbnail', fatal=False)
         sources = self._parse_json(self._search_regex(r'(?s)MEDIA\s*=\s*(\[.+?\]);', webpage, 'media'), episode, js_to_json)
 
         formats = []
@@ -67,7 +65,8 @@ def _real_extract(self, url):
 
         return {
             'id': episode,
-            'title': title,
+            'title': clean_html(self._html_search_regex(
+                r'<span[^>]+\bid=["\']clip-title["\'][^>]*>([^<]+)', webpage, 'title', fatal=False)),
             'formats': formats,
-            'thumbnail': thumbnail,
+            'thumbnail': self._search_regex(r'POSTER\s*=\s*"([^"]+)', webpage, 'thumbnail', fatal=False),
         }
diff --git a/yt_dlp/extractor/rule34video.py b/yt_dlp/extractor/rule34video.py
index 9d15f4d21..f3250b557 100644
--- a/yt_dlp/extractor/rule34video.py
+++ b/yt_dlp/extractor/rule34video.py
@@ -1,6 +1,6 @@
 import re
 
-from ..utils import parse_duration
+from ..utils import parse_duration, unescapeHTML
 from .common import InfoExtractor
 
 
@@ -16,7 +16,8 @@ class Rule34VideoIE(InfoExtractor):
                 'title': 'Shot It-(mmd hmv)',
                 'thumbnail': 'https://rule34video.com/contents/videos_screenshots/3065000/3065157/preview.jpg',
                 'duration': 347.0,
-                'age_limit': 18
+                'age_limit': 18,
+                'tags': 'count:14'
             }
         },
         {
@@ -28,7 +29,8 @@ class Rule34VideoIE(InfoExtractor):
                 'title': 'Lara in Trouble Ep. 7 [WildeerStudio]',
                 'thumbnail': 'https://rule34video.com/contents/videos_screenshots/3065000/3065296/preview.jpg',
                 'duration': 938.0,
-                'age_limit': 18
+                'age_limit': 18,
+                'tags': 'count:50'
             }
         },
     ]
@@ -57,5 +59,7 @@ def _real_extract(self, url):
             'title': title,
             'thumbnail': thumbnail,
             'duration': parse_duration(duration),
-            'age_limit': 18
+            'age_limit': 18,
+            'tags': list(map(unescapeHTML, re.findall(
+                r'<a class="tag_item"[^>]+\bhref="https://rule34video\.com/tags/\d+/"[^>]*>(?P<tag>[^>]*)</a>', webpage))),
         }
diff --git a/yt_dlp/networking/_helper.py b/yt_dlp/networking/_helper.py
index a43c57bb4..4c9dbf25d 100644
--- a/yt_dlp/networking/_helper.py
+++ b/yt_dlp/networking/_helper.py
@@ -2,6 +2,7 @@
 
 import contextlib
 import functools
+import socket
 import ssl
 import sys
 import typing
@@ -206,3 +207,59 @@ def wrapper(self, *args, **kwargs):
                 e.handler = self
             raise
     return wrapper
+
+
+def _socket_connect(ip_addr, timeout, source_address):
+    af, socktype, proto, canonname, sa = ip_addr
+    sock = socket.socket(af, socktype, proto)
+    try:
+        if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
+            sock.settimeout(timeout)
+        if source_address:
+            sock.bind(source_address)
+        sock.connect(sa)
+        return sock
+    except socket.error:
+        sock.close()
+        raise
+
+
+def create_connection(
+    address,
+    timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
+    source_address=None,
+    *,
+    _create_socket_func=_socket_connect
+):
+    # Work around socket.create_connection() which tries all addresses from getaddrinfo() including IPv6.
+    # This filters the addresses based on the given source_address.
+    # Based on: https://github.com/python/cpython/blob/main/Lib/socket.py#L810
+    host, port = address
+    ip_addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
+    if not ip_addrs:
+        raise socket.error('getaddrinfo returns an empty list')
+    if source_address is not None:
+        af = socket.AF_INET if ':' not in source_address[0] else socket.AF_INET6
+        ip_addrs = [addr for addr in ip_addrs if addr[0] == af]
+        if not ip_addrs:
+            raise OSError(
+                f'No remote IPv{4 if af == socket.AF_INET else 6} addresses available for connect. '
+                f'Can\'t use "{source_address[0]}" as source address')
+
+    err = None
+    for ip_addr in ip_addrs:
+        try:
+            sock = _create_socket_func(ip_addr, timeout, source_address)
+            # Explicitly break __traceback__ reference cycle
+            # https://bugs.python.org/issue36820
+            err = None
+            return sock
+        except socket.error as e:
+            err = e
+
+    try:
+        raise err
+    finally:
+        # Explicitly break __traceback__ reference cycle
+        # https://bugs.python.org/issue36820
+        err = None
diff --git a/yt_dlp/networking/_urllib.py b/yt_dlp/networking/_urllib.py
index 3c0647ecf..c327f7744 100644
--- a/yt_dlp/networking/_urllib.py
+++ b/yt_dlp/networking/_urllib.py
@@ -23,6 +23,7 @@
 from ._helper import (
     InstanceStoreMixin,
     add_accept_encoding_header,
+    create_connection,
     get_redirect_method,
     make_socks_proxy_opts,
     select_proxy,
@@ -54,44 +55,10 @@
 def _create_http_connection(http_class, source_address, *args, **kwargs):
     hc = http_class(*args, **kwargs)
 
+    if hasattr(hc, '_create_connection'):
+        hc._create_connection = create_connection
+
     if source_address is not None:
-        # This is to workaround _create_connection() from socket where it will try all
-        # address data from getaddrinfo() including IPv6. This filters the result from
-        # getaddrinfo() based on the source_address value.
-        # This is based on the cpython socket.create_connection() function.
-        # https://github.com/python/cpython/blob/master/Lib/socket.py#L691
-        def _create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None):
-            host, port = address
-            err = None
-            addrs = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
-            af = socket.AF_INET if '.' in source_address[0] else socket.AF_INET6
-            ip_addrs = [addr for addr in addrs if addr[0] == af]
-            if addrs and not ip_addrs:
-                ip_version = 'v4' if af == socket.AF_INET else 'v6'
-                raise OSError(
-                    "No remote IP%s addresses available for connect, can't use '%s' as source address"
-                    % (ip_version, source_address[0]))
-            for res in ip_addrs:
-                af, socktype, proto, canonname, sa = res
-                sock = None
-                try:
-                    sock = socket.socket(af, socktype, proto)
-                    if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:
-                        sock.settimeout(timeout)
-                    sock.bind(source_address)
-                    sock.connect(sa)
-                    err = None  # Explicitly break reference cycle
-                    return sock
-                except OSError as _:
-                    err = _
-                    if sock is not None:
-                        sock.close()
-            if err is not None:
-                raise err
-            else:
-                raise OSError('getaddrinfo returns an empty list')
-        if hasattr(hc, '_create_connection'):
-            hc._create_connection = _create_connection
         hc.source_address = (source_address, 0)
 
     return hc
@@ -220,13 +187,28 @@ def make_socks_conn_class(base_class, socks_proxy):
     proxy_args = make_socks_proxy_opts(socks_proxy)
 
     class SocksConnection(base_class):
-        def connect(self):
-            self.sock = sockssocket()
-            self.sock.setproxy(**proxy_args)
-            if type(self.timeout) in (int, float):  # noqa: E721
-                self.sock.settimeout(self.timeout)
-            self.sock.connect((self.host, self.port))
+        _create_connection = create_connection
 
+        def connect(self):
+            def sock_socket_connect(ip_addr, timeout, source_address):
+                af, socktype, proto, canonname, sa = ip_addr
+                sock = sockssocket(af, socktype, proto)
+                try:
+                    connect_proxy_args = proxy_args.copy()
+                    connect_proxy_args.update({'addr': sa[0], 'port': sa[1]})
+                    sock.setproxy(**connect_proxy_args)
+                    if timeout is not socket._GLOBAL_DEFAULT_TIMEOUT:  # noqa: E721
+                        sock.settimeout(timeout)
+                    if source_address:
+                        sock.bind(source_address)
+                    sock.connect((self.host, self.port))
+                    return sock
+                except socket.error:
+                    sock.close()
+                    raise
+            self.sock = create_connection(
+                (proxy_args['addr'], proxy_args['port']), timeout=self.timeout,
+                source_address=self.source_address, _create_socket_func=sock_socket_connect)
             if isinstance(self, http.client.HTTPSConnection):
                 self.sock = self._context.wrap_socket(self.sock, server_hostname=self.host)
 
diff --git a/yt_dlp/socks.py b/yt_dlp/socks.py
index f93328f63..e7f41d7e2 100644
--- a/yt_dlp/socks.py
+++ b/yt_dlp/socks.py
@@ -134,26 +134,31 @@ def _check_response_version(self, expected_version, got_version):
             self.close()
             raise InvalidVersionError(expected_version, got_version)
 
-    def _resolve_address(self, destaddr, default, use_remote_dns):
-        try:
-            return socket.inet_aton(destaddr)
-        except OSError:
-            if use_remote_dns and self._proxy.remote_dns:
-                return default
-            else:
-                return socket.inet_aton(socket.gethostbyname(destaddr))
+    def _resolve_address(self, destaddr, default, use_remote_dns, family=None):
+        for f in (family,) if family else (socket.AF_INET, socket.AF_INET6):
+            try:
+                return f, socket.inet_pton(f, destaddr)
+            except OSError:
+                continue
+
+        if use_remote_dns and self._proxy.remote_dns:
+            return 0, default
+        else:
+            res = socket.getaddrinfo(destaddr, None, family=family or 0)
+            f, _, _, _, ipaddr = res[0]
+            return f, socket.inet_pton(f, ipaddr[0])
 
     def _setup_socks4(self, address, is_4a=False):
         destaddr, port = address
 
-        ipaddr = self._resolve_address(destaddr, SOCKS4_DEFAULT_DSTIP, use_remote_dns=is_4a)
+        _, ipaddr = self._resolve_address(destaddr, SOCKS4_DEFAULT_DSTIP, use_remote_dns=is_4a, family=socket.AF_INET)
 
         packet = struct.pack('!BBH', SOCKS4_VERSION, Socks4Command.CMD_CONNECT, port) + ipaddr
 
         username = (self._proxy.username or '').encode()
         packet += username + b'\x00'
 
-        if is_4a and self._proxy.remote_dns:
+        if is_4a and self._proxy.remote_dns and ipaddr == SOCKS4_DEFAULT_DSTIP:
             packet += destaddr.encode() + b'\x00'
 
         self.sendall(packet)
@@ -210,7 +215,7 @@ def _socks5_auth(self):
     def _setup_socks5(self, address):
         destaddr, port = address
 
-        ipaddr = self._resolve_address(destaddr, None, use_remote_dns=True)
+        family, ipaddr = self._resolve_address(destaddr, None, use_remote_dns=True)
 
         self._socks5_auth()
 
@@ -220,8 +225,10 @@ def _setup_socks5(self, address):
             destaddr = destaddr.encode()
             packet += struct.pack('!B', Socks5AddressType.ATYP_DOMAINNAME)
             packet += self._len_and_data(destaddr)
-        else:
+        elif family == socket.AF_INET:
             packet += struct.pack('!B', Socks5AddressType.ATYP_IPV4) + ipaddr
+        elif family == socket.AF_INET6:
+            packet += struct.pack('!B', Socks5AddressType.ATYP_IPV6) + ipaddr
         packet += struct.pack('!H', port)
 
         self.sendall(packet)


Test patch:
diff --git a/test/test_socks.py b/test/test_socks.py
index 95ffce275..211ee814d 100644
--- a/test/test_socks.py
+++ b/test/test_socks.py
@@ -281,17 +281,13 @@ def test_socks4_auth(self, handler, ctx):
                     rh, proxies={'all': f'socks4://user:@{server_address}'})
                 assert response['version'] == 4
 
-    @pytest.mark.parametrize('handler,ctx', [
-        pytest.param('Urllib', 'http', marks=pytest.mark.xfail(
-            reason='socks4a implementation currently broken when destination is not a domain name'))
-    ], indirect=True)
+    @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_socks4a_ipv4_target(self, handler, ctx):
         with ctx.socks_server(Socks4ProxyHandler) as server_address:
             with handler(proxies={'all': f'socks4a://{server_address}'}) as rh:
                 response = ctx.socks_info_request(rh, target_domain='127.0.0.1')
                 assert response['version'] == 4
-                assert response['ipv4_address'] == '127.0.0.1'
-                assert response['domain_address'] is None
+                assert (response['ipv4_address'] == '127.0.0.1') != (response['domain_address'] == '127.0.0.1')
 
     @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_socks4a_domain_target(self, handler, ctx):
@@ -302,10 +298,7 @@ def test_socks4a_domain_target(self, handler, ctx):
                 assert response['ipv4_address'] is None
                 assert response['domain_address'] == 'localhost'
 
-    @pytest.mark.parametrize('handler,ctx', [
-        pytest.param('Urllib', 'http', marks=pytest.mark.xfail(
-            reason='source_address is not yet supported for socks4 proxies'))
-    ], indirect=True)
+    @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_ipv4_client_source_address(self, handler, ctx):
         with ctx.socks_server(Socks4ProxyHandler) as server_address:
             source_address = f'127.0.0.{random.randint(5, 255)}'
@@ -327,10 +320,7 @@ def test_socks4_errors(self, handler, ctx, reply_code):
                 with pytest.raises(ProxyError):
                     ctx.socks_info_request(rh)
 
-    @pytest.mark.parametrize('handler,ctx', [
-        pytest.param('Urllib', 'http', marks=pytest.mark.xfail(
-            reason='IPv6 socks4 proxies are not yet supported'))
-    ], indirect=True)
+    @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_ipv6_socks4_proxy(self, handler, ctx):
         with ctx.socks_server(Socks4ProxyHandler, bind_ip='::1') as server_address:
             with handler(proxies={'all': f'socks4://{server_address}'}) as rh:
@@ -342,7 +332,7 @@ def test_ipv6_socks4_proxy(self, handler, ctx):
     @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_timeout(self, handler, ctx):
         with ctx.socks_server(Socks4ProxyHandler, sleep=2) as server_address:
-            with handler(proxies={'all': f'socks4://{server_address}'}, timeout=1) as rh:
+            with handler(proxies={'all': f'socks4://{server_address}'}, timeout=0.5) as rh:
                 with pytest.raises(TransportError):
                     ctx.socks_info_request(rh)
 
@@ -383,7 +373,7 @@ def test_socks5_domain_target(self, handler, ctx):
         with ctx.socks_server(Socks5ProxyHandler) as server_address:
             with handler(proxies={'all': f'socks5://{server_address}'}) as rh:
                 response = ctx.socks_info_request(rh, target_domain='localhost')
-                assert response['ipv4_address'] == '127.0.0.1'
+                assert (response['ipv4_address'] == '127.0.0.1') != (response['ipv6_address'] == '::1')
                 assert response['version'] == 5
 
     @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
@@ -404,22 +394,15 @@ def test_socks5h_ip_target(self, handler, ctx):
                 assert response['domain_address'] is None
                 assert response['version'] == 5
 
-    @pytest.mark.parametrize('handler,ctx', [
-        pytest.param('Urllib', 'http', marks=pytest.mark.xfail(
-            reason='IPv6 destination addresses are not yet supported'))
-    ], indirect=True)
+    @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_socks5_ipv6_destination(self, handler, ctx):
         with ctx.socks_server(Socks5ProxyHandler) as server_address:
             with handler(proxies={'all': f'socks5://{server_address}'}) as rh:
                 response = ctx.socks_info_request(rh, target_domain='[::1]')
                 assert response['ipv6_address'] == '::1'
-                assert response['port'] == 80
                 assert response['version'] == 5
 
-    @pytest.mark.parametrize('handler,ctx', [
-        pytest.param('Urllib', 'http', marks=pytest.mark.xfail(
-            reason='IPv6 socks5 proxies are not yet supported'))
-    ], indirect=True)
+    @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_ipv6_socks5_proxy(self, handler, ctx):
         with ctx.socks_server(Socks5ProxyHandler, bind_ip='::1') as server_address:
             with handler(proxies={'all': f'socks5://{server_address}'}) as rh:
@@ -430,10 +413,7 @@ def test_ipv6_socks5_proxy(self, handler, ctx):
 
     # XXX: is there any feasible way of testing IPv6 source addresses?
     # Same would go for non-proxy source_address test...
-    @pytest.mark.parametrize('handler,ctx', [
-        pytest.param('Urllib', 'http', marks=pytest.mark.xfail(
-            reason='source_address is not yet supported for socks5 proxies'))
-    ], indirect=True)
+    @pytest.mark.parametrize('handler,ctx', [('Urllib', 'http')], indirect=True)
     def test_ipv4_client_source_address(self, handler, ctx):
         with ctx.socks_server(Socks5ProxyHandler) as server_address:
             source_address = f'127.0.0.{random.randint(5, 255)}'